在使用SpringCloud生态的时候,微服务之间会进行调用,一般我们有两个选择。一是选择使用RestTemplate,二是使用Feign。二者都是基于HTTP的调用,在RestTemplate上加上@LoadBalanced注解,即可生成Ribbon的代理对象,直接实现负载均衡。Feign直接使用声明式调用,更加符合大家平时写接口的习惯,下面主要针对Feign做下介绍。
1、依赖引入
<dependency><!-- consul 服务发现 -->
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency><!-- consul 服务调用 -->
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency><!-- 健康检查 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependencyManagement>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencyManagement>
2、服务端
import org.mybatis.spring.annotation.MapperScan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* @Desription: 公共配置服务启动类
* @Author: yangchenhui
*/
@SpringBootApplication
@EnableDiscoveryClient
public class CommonCfgApplication {
private static final Logger logger = LoggerFactory.getLogger(CommonCfgApplication.class);
public static void main(String[] args) {
SpringApplication.run(CommonCfgApplication.class, args);
logger.info("**************** common-cfg Server Started ****************");
}
}
服务提供方工程结构:
API接口:
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
/**
* @Desription: 参数获取
* @Author: yangchenhui
*/
@FeignClient(name = CommonCfgInfoConstant.COMMON_CFG_SERVICE_NAME, path = CommonCfgInfoConstant.COMMON_CFG_CONTEXT_PATH)
public interface ParamService {
@RequestMapping(value = "/getParamCode", method = RequestMethod.GET)
String getParamCode(@RequestParam("paramCode") String paramCode);
}
3、消费者方的使用方式
3.1、由服务方提供API包,消费端集成使用
这种方式消费者端使用比较方便,入参出参,接口定义都不用写,可以直接注入使用。缺点是需要引入jar包,可能jar中提供的接口也不是这个工程所需要的,如果是老工程的改造的话,还有可能出现同名的Class,这样导包的时候也不太方便。
3.2、由消费方自己写接口,初入参
这种方式消费者方可以选择性的使用服务提供方的接口,并且不需要引入依赖。缺点就是同样的一个接口,需要在多个工程中都写同样的代码,造成代码的重复。如果服务方接口升级,却没有通知到调用方,就有可能造成接口的不兼容。使用上一种方法,就可以通过maven来进行版本管理,可控性稍微强一点。
个人还是比较推荐使用由服务方提供API包的方式,具体使用哪种方式,大家还是跟小组成员商量共同决定。在引入其他服务的的接口时,最好加入防腐层,这样服务提供放的接口变了,我们也只需要修改防腐层即可。
消费方启动类:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
// feign的接口在哪个包下,就扫描哪个包
@EnableFeignClients(basePackages = {"x.x.x.x", "x.x.x.x.x"})
@EnableDiscoveryClient
public class ConsumerServer {
private static final Logger logger = LoggerFactory.getLogger(ConsumerServer.class);
public static void main(String[] args) {
SpringApplication.run(ConsumerServer.class, args);
logger.info("**************** consumer-server Started ****************");
}
}
再引入API的maven坐标,只要消费者和提供者是在同一个注册中心,就可以无感知的调用服务方的接口了。其实这种方式跟dubbo很像,只不过一个是rpc长连接调用,一个是基于HTTP的调用。
值得注意的是,@EnableFeignClients(basePackages = {“x.x.x.x”, “x.x.x.x.x”})这个一定要把API所在的包扫描进去,不然在引用注入的时候,feign还没有帮接口生成代理对象,使用@Autowired或者@Resource注入的时候会报错。
调用示例:
/**
* @Desription:
* @Author: yangchenhui
*/
@RestController
public class ConsTestFeginController {
@Resource
private ParamService paramService;
@PostMapping("/consTestFeign7")
Object testFeign7(@RequestBody Map<String, Object> requestParam, HttpServletRequest request) {
String paramCode = paramService.getParamCode("CREDIT_MED_STATUS");
return paramCode;
}
}
4、如何通过feign传递header参数
4.1、在接口中使用@RequestHeader注解传递
/**
* 测试有参数的feign调用
*
* @param requestParam
* @param prdCode
* @return
*/
@RequestMapping(value = "/cfg-server/testFeign3", method = RequestMethod.POST)
Object testFeign3(@RequestBody Map<String, Object> requestParam,
@RequestHeader("prdCode") String prdCode);
4.2、使用拦截器
客户端拦截器:
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @Desription: Feign请求拦截器(设置请求头)
* @Author: yangchenhui
*/
public class FeignBasicAuthRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
HttpServletRequest request = getHttpServletRequest();
if (null == request){
return;
}
Map<String,String> headers = getHeaders(getHttpServletRequest());
for(String headerName : headers.keySet()){
requestTemplate.header(headerName, getHeaders(getHttpServletRequest()).get(headerName));
}
}
private HttpServletRequest getHttpServletRequest() {
try {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
} catch (Exception e) {
return null;
}
}
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);
}
return map;
}
}
import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Desription: Feign配置注册(全局)
* @Author: yangchenhui
*/
@Configuration
public class FeignSupportConfig {
@Bean
public RequestInterceptor requestInterceptor(){
return new FeignBasicAuthRequestInterceptor();
}
}
服务端获取header参数:
/**
* @Desription:
* @Author: yangchenhui
*/
@RestController
public class ParamController {
@Resource
private ParamService paramService;
@GetMapping(value = "/getParamCode")
public String getParamCode(String paramCode, HttpServletRequest request) {
System.out.println(request.getHeader("prdCode"));
System.out.println(request.getHeader("version"));
System.out.println(request.getHeader("terminalType"));
return paramService.getParamCodeValue(paramCode);
}
}
5、超时时间配置
我们可以配置全局的、也可以针对指定服务配置,还可以将某些超时时间需要进行不同控制的抽取到一个接口当中,通过contextId进行配置管理。
单独抽离接口:
/**
* @Desription: 将单独控制超时时间的接口独立出来,通过contextId在配置文件中指定配置参数
* @Author: yangchenhui
*/
@Component
@FeignClient(name = "cfg-server", fallback = CfgServiceImpl.class, contextId = "create-order")
public interface CreateOrderService {
/**
* 测试有参数的feign调用
*
* @param requestParam
* @return
*/
@PostMapping(value = "/cfg-server/testFeign5")
Object testFeign5(@RequestBody Map<String, Object> requestParam);
}
yml文件配置:
feign:
hystrix:
enabled: true
httpclient:
connection-timeout: 2500
client:
config:
#default代表所有服务
default:
#feign客户端建立连接超时时间
connect-timeout: 2000
#feign客户端建立连接后读取资源超时时间
read-timeout: 2000
#cfg-server表示当调用cfg-server这个服务时,用下面的配置
cfg-server:
connectTimeout: 2000
readTimeout: 2000
loggerLevel: full
x-x-x-server:
connectTimeout: 2000
readTimeout: 2000
loggerLevel: full
# errorDecoder: com.example.SimpleErrorDecoder
# retryer: com.example.SimpleRetryer
# requestInterceptors:
# - com.example.FooRequestInterceptor
# - com.example.BarRequestInterceptor
# decode404: false
# encoder: com.example.SimpleEncoder
# decoder: com.example.SimpleDecoder
# contract: com.example.SimpleContract
# contextId = create-order的接口,单独配置超时时间
create-order:
#feign客户端建立连接超时时间
connect-timeout: 1000
#feign客户端建立连接后读取资源超时时间
read-timeout: 1000
hystrix:
command:
default:
execution:
isolation:
strategy: SEMAPHORE