组件整合之原生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";
}

 

posted @ 2022-01-04 08:53  残城碎梦  阅读(354)  评论(0编辑  收藏  举报