组件整合之原生Feign服务调用
Feign是一个声明式的Web服务客户端,使用Feign可使得Web服务客户端的写入更加方便。它具有可插拔注释支持,包括Feign注解和JAX-RS注解、Feign还支持可插拔编码器和解码器。
Feign提供了Feign.builder()客户端的构造方法,可以轻松的访问远程的URL,不依赖其他服务。这种方式的用法是将远程的URL,封装成纯接口SDK,供其他用户或系统使用。
Feign快速入门
pom依赖
<!--feign依赖-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
<version>11.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-jackson</artifactId>
<version>11.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hystrix</artifactId>
<version>11.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
<version>11.0</version>
</dependency>
创建Feign配置
import feign.Feign;
import feign.Request;
import feign.Retryer;
import feign.jackson.JacksonDecoder;
import feign.jackson.JacksonEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;
/**
* @ClassName RestfulFeignConfig.java
* @Description feign配置
*/
@Configuration
public class RestfulFeignConfig {
@Bean
public DemoClient demoClient(){
DemoClient client = Feign.builder()
.options(new Request.Options(15, TimeUnit.SECONDS, 120, TimeUnit.SECONDS, true))//设置 链接超时不设默认10秒 回复超时不设默认60秒
.logger(new Logger.ErrorLogger()).logLevel(Logger.Level.FULL) // 打印所有日志
.retryer(new Retryer.Default(6000, 6000, 3))
.client(new feign.okhttp.OkHttpClient(getNoCertHttpClient()))//需要支持https的client
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.target(DemoClient.class, "http://localhost:8899"); //接口及请求地址
return client;
}
/**
* 无证书https okhttp配置
*
* @return
*/
private okhttp3.OkHttpClient getNoCertHttpClient() {
final X509TrustManager trustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
SSLContext sslContext = null;
SSLSocketFactory sslSocketFactory = null;
try {
sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, new TrustManager[]{trustManager}, new SecureRandom());
sslSocketFactory = sslContext.getSocketFactory();
} catch (Exception e) {
e.printStackTrace();
return null;
}
okhttp3.OkHttpClient client = new okhttp3.OkHttpClient().newBuilder()
.sslSocketFactory(sslSocketFactory, trustManager)
.build();
return client;
}
}
DemoClient.java
import feign.Headers;
import feign.QueryMap;
import feign.RequestLine;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.Map;
public interface DemoClient {
@RequestLine("POST /demo/listOrderInfo")
@Headers({"content-type: application/json;charset=utf-8"})
Map<String, Object> listOrderInfo(@RequestBody Map<String, Object> param);
@RequestLine("GET /demo/getOrderInfoById")
@Headers("content-type: application/x-www-form-urlencoded")
Map<String, Object> getOrderInfoById(@QueryMap Map<String, Object> params);
}
测试
1、重新创建一个web工程(下称工程B),创建一个FeignController控制类,如下:
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/demo")
public class FeignController {
private static Map<Long, OrderInfo> orderInfoMap = new HashMap();
static {
orderInfoMap.put(1L, new OrderInfo(1L, "订单1"));
orderInfoMap.put(2L, new OrderInfo(2L, "订单2"));
orderInfoMap.put(3L, new OrderInfo(3L, "订单3"));
orderInfoMap.put(4L, new OrderInfo(4L, "订单4"));
orderInfoMap.put(5L, new OrderInfo(5L, "订单5"));
orderInfoMap.put(6L, new OrderInfo(6L, "订单6"));
orderInfoMap.put(7L, new OrderInfo(7L, "订单7"));
orderInfoMap.put(8L, new OrderInfo(8L, "订单8"));
}
@PostMapping("listOrderInfo")
public Map<String, Object> listOrderInfo(@RequestBody Map<String, Object> param) {
log.info("listOrderInfo参数为:{}", JSON.toJSONString(param));
Map<String, Object> resultMap = new HashMap();
resultMap.put("success", true);
resultMap.put("code", 200);
resultMap.put("data", orderInfoMap.values());
return resultMap;
}
@GetMapping("getOrderInfoById")
public Map<String, Object> getOrderInfoById(Long orderId, String orderDesc) {
log.info("getOrderInfoById参数orderId={}, orderDesc={}", orderId, orderDesc);
Map<String, Object> resultMap = new HashMap();
resultMap.put("success", true);
resultMap.put("code", 200);
resultMap.put("data", orderInfoMap.get(orderId));
return resultMap;
}
static class OrderInfo implements Serializable {
private static final long serialVersionUID = -1697465489378771008L;
private Long orderId;
private String orderDesc;
public OrderInfo() {
}
public OrderInfo(Long orderId, String orderDesc) {
this.orderId = orderId;
this.orderDesc = orderDesc;
}
public void setOrderId(Long orderId) {
this.orderId = orderId;
}
public Long getOrderId() {
return orderId;
}
public void setOrderDesc(String orderDesc) {
this.orderDesc = orderDesc;
}
public String getOrderDesc() {
return orderDesc;
}
}
}
2、调用工程B的接口
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={RootConfig.class, WebMvcConfig.class}) //如果是Java Config配置的,指定相关的配置类
@WebAppConfiguration
public class DemoClientTest {
@Autowired
private DemoClient demoClient;
@Test
public void testListOrderInfo(){
Map<String, Object> params = new HashMap();
params.put("pageSize", 10);
params.put("pageNo", 1);
Map<String, Object> respMap = demoClient.listOrderInfo(params);
System.out.println("listOrderInfo返回结果:" + JsonUtil.obj2String(respMap));
}
@Test
public void testGetOrderInfo(){
Map<String, Object> params = new HashMap();
params.put("orderId", 1);
Map<String, Object> respMap = demoClient.getOrderInfoById(params);
System.out.println("getOrderInfoById返回结果:" + JsonUtil.obj2String(respMap));
}
}
使用jackson进行json和实体转换的工具类
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.text.SimpleDateFormat;
@Slf4j
public class JsonUtil {
private static ObjectMapper objectMapper = new ObjectMapper();
// 日起格式化
private static final String STANDARD_FORMAT = "yyyy-MM-dd HH:mm:ss";
static {
//对象的所有字段全部列入
objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
//取消默认转换timestamps形式
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
//忽略空Bean转json的错误
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
//所有的日期格式都统一为以下的样式,即yyyy-MM-dd HH:mm:ss
objectMapper.setDateFormat(new SimpleDateFormat(STANDARD_FORMAT));
//忽略 在json字符串中存在,但是在java对象中不存在对应属性的情况。防止错误
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
/**
* 对象转Json格式字符串
*
* @param obj 对象
* @return Json格式字符串
*/
public static <T> String obj2String(T obj) {
if (obj == null) {
return null;
}
try {
return obj instanceof String ? (String) obj : objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
log.warn("Parse Object to String error : {}", e.getMessage());
return null;
}
}
/**
* 对象转Json格式字符串(格式化的Json字符串)
*
* @param obj 对象
* @return 美化的Json格式字符串
*/
public static <T> String obj2StringPretty(T obj) {
if (obj == null) {
return null;
}
try {
return obj instanceof String ? (String) obj : objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
} catch (JsonProcessingException e) {
log.warn("Parse Object to String error : {}", e.getMessage());
return null;
}
}
/**
* 字符串转换为自定义对象
*
* @param str 要转换的字符串
* @param clazz 自定义对象的class对象
* @return 自定义对象
*/
public static <T> T string2Obj(String str, Class<T> clazz) {
if (StringUtils.isEmpty(str) || clazz == null) {
return null;
}
try {
return clazz.equals(String.class) ? (T) str : objectMapper.readValue(str, clazz);
} catch (Exception e) {
log.warn("Parse String to Object error : {}", e.getMessage());
return null;
}
}
public static <T> T string2Obj(String str, TypeReference<T> typeReference) {
if (StringUtils.isEmpty(str) || typeReference == null) {
return null;
}
try {
return (T) (typeReference.getType().equals(String.class) ? str : objectMapper.readValue(str, typeReference));
} catch (IOException e) {
log.warn("Parse String to Object error", e);
return null;
}
}
public static <T> T string2Obj(String str, Class<?> collectionClazz, Class<?>... elementClazzes) {
JavaType javaType = objectMapper.getTypeFactory().constructParametricType(collectionClazz, elementClazzes);
try {
return objectMapper.readValue(str, javaType);
} catch (IOException e) {
log.warn("Parse String to Object error : {}" + e.getMessage());
return null;
}
}
}
Feign注解
Feign 的默认定义了以下注解:
注解 | 作用位置 | 用法 |
@RequestLine | 方法 | 为请求定义HttpMethod和UriTemplate。 花括号{expression}中的值使用其相应的带@Param注解的参数解析。 |
@Param | 参数 | 定义一个模板变量,其值将用于解析相应的表达式模板,通过作为注解值提供的名称。如果缺少值,它将尝试从字节码方法参数名称中获取名称(如果代码是用-parameters标志编译的)。 |
@Headers | 方法、类 | 定义一个HeaderTemplate。使用带@Param注释的值来解析相应的Expressions. 在 Type上使用时,模板将应用于每个请求 |
@QueryMap | 参数 | 定义一个Map名称-值对或 POJO,以扩展为查询字符串。 |
@HeaderMap | 参数 | 定义一个Map名称-值对,扩展为Http Headers |
@Body | 方法 | 定义 Template,类似于UriTemplateand、 HeaderTemplate,它使用带@Param注释的值来解析相应的Expressions |
@RequestLine
RequestLine是请求行的意思,我们知道在HTTP 中,请求行一般由以下几部分构成。
@RequestLine注解只能标注在方法上,为请求定义HttpMethod(请求方式 GET、POST等)和UriTemplate(访问路径),然后可以将@Param注解中表示的参数,解析到表达式{ }对应的位置上。
@RequestLine注解有以下几个配置项,一般我们只需要配置value值就可以了。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestLine {
// 请求行的值
String value();
// 斜杠转义,默认true
boolean decodeSlash() default true;
// URL参数中编码方式,默认使用 &aa=aabb&bb=bb,不需要改
CollectionFormat collectionFormat() default CollectionFormat.EXPLODED;
}
@Param
@Param 只能做用于方法参数上,它的主要作用是,通过变量名,绑定一个参数值,然后可以将参数填充到模板表达式中。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
public @interface Param {
// Key,对应模板表达式中的变量
String value() default "";
// 填充方式,默认直接使用ToString方法,
Class<? extends Param.Expander> expander() default Param.ToStringExpander.class;
// 是否转义,默认false
/** @deprecated */
boolean encoded() default false;
// 内部类填充器 使用ToString方法填充
public static final class ToStringExpander implements Param.Expander {
public ToStringExpander() {
}
public String expand(Object value) {
return value.toString();
}
}
public interface Expander {
String expand(Object var1);
}
}
示例:会将name参数,填充到{name} 表达式中。
@RequestLine("GET /app1/test?name={name}")
String test(@Param("name") String name);
@Headers
@Headers可作用于方法或者类上,用于添加请求头。
它的属性很简单,是一个字符串数组。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Headers {
// 请求头
String[] value();
}
示例:注意添加的时候,消息头键值对使用冒号:分隔。
@Headers({"Accept:*/*", "Accept-Language:zh-cn"})
@QueryMap
@QueryMap注解只可作用于方法参数上,表示将多个参数拼接在URL后面访问。
其只有一个属性,表示是否转义,默认false。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface QueryMap {
boolean encoded() default false;
}
示例:
如果某个请求接口需要多个参数,这个时候就可以使用@QueryMap注解传递,注意这个会后参数必须是Map类型的。
比如我们可以通过以下方式传递:
@RequestLine("GET /app1/test")
@Headers({"Accept:*/*", "Accept-Language:zh-cn"})
String testQueryMap(@QueryMap Map<String, String> map);
最终会将Map中的键值对拼接到了URL后面。
@HeaderMap
@HeaderMap注解只能做用于方法参数上,和@QueryMap差不多,是将Map中的键值对,添加到请求头中。
该注解没有配置项:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface HeaderMap {
}
示例:
@RequestLine("GET /app1/test")
String testQueryMap(@HeaderMap Map<String, String> map);
@Body
@Body 作用于方法上,会使用请求体来传递参数。可以传递字符串、Json数据,在传递Bean 对象时,会调用其toString方法再进行传递。
该注解只有一个属性值,可以写表达式,通过@Param传值。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Body {
String value();
}
服务端代码
@RequestMapping("/jsonBody")
@ResponseBody
public Object jsonBody(@RequestBody JSONObject body){
System.out.println(body);
return "success";
}
客户端代码
使用String作为方法参数
@Body注解支持"{}"形式的占位符,会将方法中的参数填充到模板中
interface FeignClientAPI {
@RequestLine("POST /jsonBody")
@Headers("Content-Type: application/json")
@Body("{body}")
String jsonBody(@Param("body") String body);
}
调用:
//客户端调用
JSONObject body = new JSONObject();
body.put("id", "xxId");
body.put("name", "xxName");
feignClient.jsonBody(body.toJSONString());
使用Bean作为方法参数(toString)
feign方法参数使用String类型总感觉诸多不便,通过翻阅代码(详见feign.template.Expressions.SimpleExpression#expand(java.lang.Object, boolean)),发现可以直接使用对象参数,不过使用的对象需要重写toString方法,因为内部实现是调用对象的toString转成字符串处理。
interface FeignClientAPI {
@RequestLine("POST /jsonBody")
@Headers("Content-Type: application/json")
@Body("{body}")
String jsonBody(@Param("body") InitBean bean);
}
@Data
public class InitBean {
private String id;
private String name;
public String toString(){
return com.alibaba.fastjson.JSON.toJSONString(this);
}
}
客户端调用时,直接传Bean:
//客户端调用
InitBean bean = new InitBean ().setId("xxId").setName("xxName");
feignClient.jsonBody(bean);
将请求体与其他形式的参数配合使用
下面有三段扩展示例,每个示例包括客户端代码和服务端代码
/***************** 扩展一:请求体 + Restful风格URL ******************/
//客户端代码
@RequestLine("POST /jsonBody/{path}")
@Headers("Content-Type: application/json")
@Body("{body}")
String jsonBodyAndPath(@Param("body") String body, @Param(value="path") String path);
//服务端代码
@RequestMapping("/jsonBody/{path}")
@ResponseBody
public Object jsonBodyAndPath(@RequestBody JSONObject body, @PathVariable String path){
System.out.println(body);
System.out.println(path);
return "success";
}
/***************** 扩展二:请求体 + 请求参数 ******************/
//客户端代码
@RequestLine("POST /jsonBodyWithParam")
@Headers("Content-Type: application/json")
@Body("{body}")
String jsonBodyWithParam(@Param("body") String body, @QueryMap Map map);
//服务端代码
@RequestMapping("/jsonBodyWithParam")
@ResponseBody
public Object jsonBodyWithParam(@RequestBody JSONObject body, @RequestParam String param){
System.out.println(body);
System.out.println(param);
return "success";
}
/***************** 扩展三:请求体 + Restful风格URL + 请求参数 ******************/
//客户端代码
@RequestLine("POST /jsonBodyAndPathWithParam/{path}")
@Headers("Content-Type: application/json")
@Body("{body}")
String jsonBodyAndPathWithParam(@Param("body") String body, @Param(value="path") String path, @QueryMap Map map);
//服务端代码
@RequestMapping("/jsonBodyAndPathWithParam/{path}")
@ResponseBody
public Object jsonBodyAndPathWithParam(@RequestBody JSONObject body, @PathVariable String path, @RequestParam String param){
System.out.println(body);
System.out.println(path);
System.out.println(param);
return "success";
}