使用Feign实现声明式REST调用
6.1. Feign简介
Feign是一个声明式的REST客户端,它的目的就是让REST调用更加简单。
Feign提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息。
而Feign则会完全代理HTTP请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。
SpringCloud对Feign进行了封装,使其支持SpringMVC标准注解和HttpMessageConverters。
Feign可以与Eureka和Ribbon组合使用以支持负载均衡。
6.2. 为服务消费者整合Feign
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <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> <artifactId>microservice-consumer-movie-feign</artifactId> <packaging>jar</packaging> <parent> <groupId>com.itmuch.cloud</groupId> <artifactId>microservice-spring-cloud</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</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-feign</artifactId> </dependency> </dependencies> </project>
Feign类
package com.itmuch.cloud.feign; import org.springframework.cloud.netflix.feign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import com.itmuch.cloud.entity.User; @FeignClient("microservice-provider-user") public interface UserFeignClient { @RequestMapping(value = "/simple/{id}", method = RequestMethod.GET) //@GetMapping("/simple/{id}") public User findById(@PathVariable("id") Long id); // 两个坑:1. @GetMapping不支持 2. @PathVariable得设置value }
图文:
6.3. 自定义Feign配置
Spring Cloud允许通过注解@FeignClient的configuration属性自定义Feign的配置,自定义配置的优先级比FeignClientsConfiguration要高。
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <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> <artifactId>microservice-consumer-movie-feign-customizing</artifactId> <packaging>jar</packaging> <parent> <groupId>com.itmuch.cloud</groupId> <artifactId>microservice-spring-cloud</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</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-feign</artifactId> </dependency> </dependencies> </project>
配置文件
spring: application: name: microservice-consumer-movie-feign-customizing server: port: 7901 eureka: client: healthcheck: enabled: true serviceUrl: defaultZone: http://user:password123@localhost:8761/eureka instance: prefer-ip-address: true logging: level: com.itmuch.cloud.feign.UserFeignClient: DEBUG # 解决第一次请求报超时异常的方案: # hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 5000 # 或者: # hystrix.command.default.execution.timeout.enabled: false # 或者: feign.hystrix.enabled: false ## 索性禁用feign的hystrix支持 # 超时的issue:https://github.com/spring-cloud/spring-cloud-netflix/issues/768 # 超时的解决方案: http://stackoverflow.com/questions/27375557/hystrix-command-fails-with-timed-out-and-no-fallback-available # hystrix配置: https://github.com/Netflix/Hystrix/wiki/Configuration#execution.isolation.thread.timeoutInMilliseconds
config
package com.itmuch.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import feign.Contract; @Configuration public class FooConfiguration { @Bean public Contract feignContract() { return new feign.Contract.Default(); } }
实体
package com.itmuch.cloud.entity; import java.math.BigDecimal; public class User { private Long id; private String username; private String name; private Short age; private BigDecimal balance; public Long getId() { return this.id; } public void setId(Long id) { this.id = id; } public String getUsername() { return this.username; } public void setUsername(String username) { this.username = username; } public String getName() { return this.name; } public void setName(String name) { this.name = name; } public Short getAge() { return this.age; } public void setAge(Short age) { this.age = age; } public BigDecimal getBalance() { return this.balance; } public void setBalance(BigDecimal balance) { this.balance = balance; } }
Feign
package com.itmuch.cloud.feign; import org.springframework.cloud.netflix.feign.FeignClient; import com.itmuch.cloud.entity.User; import com.itmuch.config.FooConfiguration; import feign.Param; import feign.RequestLine; @FeignClient(name = "microservice-provider-user", configuration = FooConfiguration.class) public interface UserFeignClient { @RequestLine("GET /simple/{id}") public User findById(@Param("id") Long id); }
Controller
package com.itmuch.cloud.controller; 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.itmuch.cloud.entity.User; import com.itmuch.cloud.feign.UserFeignClient; @RestController public class MovieController { @Autowired private UserFeignClient userFeignClient; @GetMapping("/movie/{id}") public User findById(@PathVariable Long id) { return this.userFeignClient.findById(id); } }
启动类
package com.itmuch.cloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.netflix.feign.EnableFeignClients; @SpringBootApplication @EnableEurekaClient @EnableFeignClients public class ConsumerMovieFeignApplication { public static void main(String[] args) { SpringApplication.run(ConsumerMovieFeignApplication.class, args); } }
注意,我们在此类中修改了Feign的Contract ,那么Contract 是什么呢。它叫做契约。因为Feign一开始使用的契约是SpringMVC,所以刚才我们SpringMVC的注解的时候直接成功了,但是你如果现在启动项目你就会发现已经启动不了了。因为Contract.Default()使用的契约是Feign自己的,也就是说我们要把SpringMVC的注解修改为Feign的注解
SpringMVC版本
@GetMapping (value = "/user/getUser/{id}") public User getUser(@PathVariable("id")Long id);
Feign版本
@RequestLine("GET /user/getUser/{id}") public User getUser(@Param("id") Long id);
6.4. 手动创建Feign
服务端:
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <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> <artifactId>microservice-provider-user-with-auth</artifactId> <packaging>jar</packaging> <name>microservice-provider-user-with-auth</name> <description>Demo project for Spring Boot</description> <parent> <groupId>com.itmuch.cloud</groupId> <artifactId>microservice-spring-cloud</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </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.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies> </project>
配置文件:
server: port: 7900 spring: jpa: generate-ddl: false show-sql: true hibernate: ddl-auto: none datasource: platform: h2 schema: classpath:schema.sql data: classpath:data.sql application: name: microservice-provider-user-with-auth logging: level: root: INFO org.hibernate: INFO org.hibernate.type.descriptor.sql.BasicBinder: TRACE org.hibernate.type.descriptor.sql.BasicExtractor: TRACE com.itmuch: DEBUG eureka: client: healthcheck: enabled: true serviceUrl: defaultZone: http://user:password123@localhost:8761/eureka instance: prefer-ip-address: true instance-id: ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}} metadata-map: zone: ABC # eureka可以理解的元数据 lilizhou: BBC # 不会影响客户端行为 lease-renewal-interval-in-seconds: 5
Spring Security的配置类
package com.itmuch.cloud.entity; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.ArrayList; import java.util.Collection; public class SecurityUser implements UserDetails { private static final long serialVersoinUID = 1L; private Long id; private String username; private String password; private String role; public SecurityUser() { } public SecurityUser(String username, String password, String role) { this.username = username; this.password = password; this.role = role; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); SimpleGrantedAuthority authority = new SimpleGrantedAuthority(this.role); authorities.add(authority); return authorities; } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public String getRole() { return role; } public void setRole(String role) { this.role = role; } }
package com.itmuch.cloud.microserviceprovideruserwithauth.security; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; @Component public class CustomUserDetailsService implements UserDetailsService { /** * 模拟两个账户 * ① 账号是user,密码是password1,角色是user-role * ② 账号时候admin,密码是password1,角色是admin-role * @param username * 用户名 * @return * * @throws UsernameNotFoundException */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { if ("user".equals(username)) { return new SecurityUser("user", "password1", "user-role"); } else if ("admin".equals(username)) { return new SecurityUser("admin", "password2", "admin-role"); } else { return null; } } }
package com.itmuch.cloud.microserviceprovideruserwithauth.security; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomUserDetailsService userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { // 所有的请求,都需要经过HTTP basic认证 http .authorizeRequests() .anyRequest().authenticated() .and() .httpBasic(); } @Bean public PasswordEncoder passwordEncoder() { // 明文编码器。这个一个不做任何操作的密码编码器,是Spring提供给我们做明文测试的 return NoOpPasswordEncoder.getInstance(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(this.userDetailsService).passwordEncoder(this.passwordEncoder()); } }
修改Controller,在其中打印当前登录的用户信息
package com.itmuch.cloud.controller; import com.itmuch.cloud.repository.UserRepository; import com.itmuch.cloud.entity.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import java.util.Collection; @RestController public class UserController { private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class); @Autowired private UserRepository userRepository; @GetMapping("/{id}") public User findById(@PathVariable Long id) { Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { UserDetails user = (UserDetails) principal; Collection<? extends GrantedAuthority> collections = user.getAuthorities(); for (GrantedAuthority ga: collections) { // 打印当前登录用户的信息 UserController.LOGGER.info("当前用户是{}, 角色是{}", user.getUsername(), ga.getAuthority()); } } else { UserController.LOGGER.warn("ε=(´ο`*)))唉,出现问题了"); } User findOne = userRepository.findOne(id); return findOne; } }
客服端:
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <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> <artifactId>microservice-consumer-movie-feign-manual</artifactId> <packaging>jar</packaging> <parent> <groupId>com.itmuch.cloud</groupId> <artifactId>microservice-spring-cloud</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</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-feign</artifactId> </dependency> </dependencies> </project>
配置文件
spring: application: name: microservice-consumer-movie-feign-manual server: port: 7901 eureka: client: healthcheck: enabled: true serviceUrl: defaultZone: http://user:password123@localhost:8761/eureka instance: prefer-ip-address: true ribbon: eureka: enabled: true
实体类
package com.itmuch.cloud.entity; import java.math.BigDecimal; public class User { private Long id; private String username; private String name; private Short age; private BigDecimal balance; public Long getId() { return this.id; } public void setId(Long id) { this.id = id; } public String getUsername() { return this.username; } public void setUsername(String username) { this.username = username; } public String getName() { return this.name; } public void setName(String name) { this.name = name; } public Short getAge() { return this.age; } public void setAge(Short age) { this.age = age; } public BigDecimal getBalance() { return this.balance; } public void setBalance(BigDecimal balance) { this.balance = balance; } }
Feign类
package com.itmuch.cloud.feign; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import com.itmuch.cloud.entity.User; public interface UserFeignClient { @RequestMapping(value = "/{id}", method = RequestMethod.GET) public User findById(@PathVariable("id") Long id); }
package com.itmuch.cloud.controller; import com.itmuch.cloud.feign.UserFeignClient; import com.itmuch.cloud.entity.User; import feign.Client; import feign.Contract; import feign.Feign; import feign.auth.BasicAuthRequestInterceptor; import feign.codec.Decoder; import feign.codec.Encoder; import org.springframework.cloud.netflix.feign.FeignClientsConfiguration; import org.springframework.context.annotation.Import; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @Import(FeignClientsConfiguration.class) // Spring Cloud为Feign默认提供的配置类 @RestController public class MovieController { private UserFeignClient userUserFeignClient; private UserFeignClient adminUserFeignClient; public MovieController(Decoder decoder, Encoder encoder, Client client, Contract contract) { this.userUserFeignClient = Feign.builder().client(client).encoder(encoder).decoder(decoder).contract(contract) .requestInterceptor(new BasicAuthRequestInterceptor("user", "password1")) .target(UserFeignClient.class, "http://microservice-provider-user-with-auth/"); this.adminUserFeignClient = Feign.builder().client(client).encoder(encoder).decoder(decoder).contract(contract) .requestInterceptor(new BasicAuthRequestInterceptor("admin", "password2")) .target(UserFeignClient.class, "http://microservice-provider-user-with-auth/"); } @GetMapping("/user-user/{id}") public User findByIdUser(@PathVariable Long id) { return this.userUserFeignClient.findById(id); } @GetMapping("/user-admin/{id}") public User findByIdAdmin(@PathVariable Long id) { return this.adminUserFeignClient.findById(id); } }
图文:
6.5. Feign对继承的支持
Feign还支持继承,将一些公共操作弄到父接口,从而简化开发
比如,先写一个基础接口:UserService.java
public interface UserService { @RequestMapping(method=RequestMethod.GET,value="/user/{id}") User getUser(@PathVariable("id") long id); }
服务提供者Controller:UserResource.java
@RestController public class UserResource implements UserService { //... }
服务消费者:UserClient.java
@FeignClient("users") public interface UserClient extends UserService { }
6.6. Feign对压缩的支持
feign: compression: mime-types: text/xml,application/xml,application/json request: enable: true min-request-size: 2048 response: enable: true
6.7. Feign的日志
Feign配置类
package com.itmuch.cloud.conf; import feign.Logger; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * Created by 2YSP on 2018/7/18. */ @Configuration public class FeignLogConfiguration { /** * NONE:不记录任何日志(默认) * BASIC:仅记录请求方法、URL、响应状态代码以及执行时间 * HEADERS:记录BASIC级别的基础上,记录请求和响应的header * FULL:记录请求和响应的header,body和元数据 * @return */ @Bean Logger.Level feignLoggerLevel(){ return Logger.Level.FULL; } }
修改Feign,使用指定配置类
package com.itmuch.cloud.feign; import org.springframework.cloud.netflix.feign.FeignClient; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import com.itmuch.cloud.conf.FeignLogConfiguration; import com.itmuch.cloud.entity.User; @FeignClient( name = "microservice-provider-user",configuration = FeignLogConfiguration.class) public interface UserFeignClient { @RequestMapping(value = "/simple/{id}", method = RequestMethod.GET) //@GetMapping("/simple/{id}") public User findById(@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 eacdy0000@126.com @RequestMapping(value = "/get-user", method = RequestMethod.GET) public User getUser(User user); }
application.yml中添加如下内容,设置日志级别,注意:Feign的日志打印只会对DEBUG级别做出响应
spring: application: name: microservice-consumer-movie-feign server: port: 7901 eureka: client: healthcheck: enabled: true serviceUrl: defaultZone: http://user:password123@localhost:8761/eureka instance: prefer-ip-address: true logging: level: com.itmuch.cloud.feign.UserFeignClient: DEBUG
6.8. 使用Feign构造多参数请求
当我们用Get请求多参数的URL的时候,比如:http://microservice-provider-user/get?id=1&username=zhangsan,可能会采取如下的方式
@FeignClient(name = "microservice-provider-user") public interface UserFeignClient { @RequestMapping(value = "/get",method = RequestMethod.GET) User get0(User user); }
正确处理方式一:使用@RequestParam注解
@RequestMapping(value = "/get",method = RequestMethod.GET) User get1(@RequestParam("id") Long id,@RequestParam("username") String username);
但是这种方法也有个缺点,如果参数比较多就要写很长的参数列表。
正确处理方式二:使用map接收
@RequestMapping(value = "/get",method = RequestMethod.GET) User get2(Map<String,Object> map);
处理方式三:如果请求方式没有限制的话,换成POST方式
@RequestMapping(value = "/get",method = RequestMethod.POST) User get3(User user);