1

使用SpringCloud的Feign组件能够为服务间的调用节省编码时间并提高开发效率,当服务本身不复杂时可以单独将该组件拿出使用。

引入依赖

<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign -->
<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-openfeign</artifactId>
 <version>2.0.4.RELEASE</version>
</dependency>

引入SpringBoot打包的Feign依赖,需要注意的是Feign的版本与SpringBoot版本的对应关系,老版本的Feign并不叫openfeign。由于我是用的SpringBoot版本是2.0x,所以openfeign使用了2.0x版本,若使用诸如2.1x或其他高版本的openfeign,在项目启动时会报“抽象方法错误”这类的异常。

编写接口作为服务调用入口

import com.bhz.demo.client.domain.req.ProductReceiveReq;
import com.bhz.demo.client.domain.resp.MemberPointBaseResp;
import com.bhz.demo.client.domain.resp.UserPointResp;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
 * @Author guomz 
 * @create 2021/3/15 14:50 
 */
@FeignClient(url = "www.123.com", name = "demoClient",configuration={DemoFeignConfiguration.class})
public interface DemoClient {

    @RequestMapping(value = "/demo/user/{uuid}/{name}", method = RequestMethod.GET)
    DemoBaseResp<DemoUserResp> getUser(@PathVariable("uuid") String uuid, @PathVariable("name") String name);
    
    @RequestMapping(value = "/demo/buy", method = RequestMethod.POST)
    DemoBaseResp buyProduct(DemoBuyReq req);
}

Feign的服务调用编写类似mybatis的dao接口,接口上方需要标注@FeignClient注解,该注解有urlnamevalue三个重要参数。其中namevalue等效,必须填写一个。在微服务环境下namevalue填写用于被注册中心发现的服务名,例如调用的用户服务叫userService则此处填写userService,此使url可以不填写,因为已经指定了调用方。url则是直接指定服务的全路径,若同时填写urlname,则以url为准,name便被当作当前客户端的名称。
上面的示例并不属于复杂的微服务环境,所以采用直接指定url来调用其他服务。
方法定义上与controller基本一致,需要注意get方法传递多个参数时需要用@RequestParam注解标注每个参数,post请求中@RequestBody标注的参数只能有一个,否则会报Method has too many Body parameters异常。
configuration参数需要特别注意,它的作用是让当前feign使用指定的配置类作为配置,不指定则使用默认配置。

启动类上加注解

为了使被标注@FeignClient注解的接口被注入到容器,需要在启动类上加@EnableFeignClients注解。

调用服务

@Service
@Slf4j
public class DemoService {
 @Autowired
 private DemoClient demoClient;
 public void getUser(Long id){
        demoClient.getUser("123", "abc");
 }
}

在需要调用其他服务的模块中引入之前定义的接口即可。

关于调用https接口

调用https接口时会进行证书校验,若没有证书则会抛出No subject alternative names present异常,可以使用以下代码来绕过证书校验:

<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-ribbon -->
<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
 <version>2.0.4.RELEASE</version>
</dependency>

首先需要引入Ribbon依赖,在绕过证书的代码中存在一些需要被注入的类属于Ribbon。Ribbon的引入同样需要注意版本问题。

import feign.Client;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.openfeign.ribbon.CachingSpringLoadBalancerFactory;
import org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.net.ssl.*;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
/**feign client配置
 * @Author guomz
 * @create 2021/3/16 9:52
 */
 @Configuration
public class FeignConfiguration {

/**
 * 调用https接口时绕过ssl证书验证
 * @param cachingFactory
 * @param clientFactory
 * @return
 * @throws NoSuchAlgorithmException
 * @throws KeyManagementException
 */
 @Bean
  public Client feignClient(@Qualifier("cachingLBClientFactory") CachingSpringLoadBalancerFactory cachingFactory, SpringClientFactory clientFactory) throws NoSuchAlgorithmException, KeyManagementException {
        SSLContext ctx = SSLContext.getInstance("TLSv1.2");
        X509TrustManager tm = new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] x509Certificates, String s) {
                    }
                @Override
                public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {
                    }
                @Override
                public X509Certificate[] getAcceptedIssuers() {
                        return new X509Certificate[0];
                 }
        };
        ctx.init(null, new TrustManager[]{tm}, null);
        return new LoadBalancerFeignClient(new Client.Default(ctx.getSocketFactory(), new HostnameVerifier() {
        @Override
        public boolean verify(String hostname, SSLSession sslSession) {
                        return true;
        }
                }),
        cachingFactory, clientFactory);
     }
}

上面是Feign的配置类,用来绕过https证书校验。

使用配置类的注意事项

上面的配置类被@Configuration注解标注,此时该配置类会作为Feign的全局配置起作用,与在启动类的@EnableFeignClients注解中配置defaultConfiguration属性效果一样。如果需要对每个Feign进行差异化配置则需要去掉@Configuration注解(defaultConfiguration同理),然后在每个@FeignClient注解配置configuration属性来指定配置类。

请求响应日志的输出与全局配置请求header

在刚才的配置类中添加下面两个方法:

/**
 * 输出请求响应日志信息
 * @return
 */@Bean
Logger.Level feignLoggerLevel() {
    return Logger.Level.FULL;
}
/**
 * 为所有请求的header中添加token
 * @return */@Bean
public RequestInterceptor getRequestInterceptor(){
    return new RequestInterceptor() {
        @Override
        public void apply(RequestTemplate requestTemplate) {
            Map<String, Collection<String>> headerMap = new HashMap<>();
            List<String> tokenList = Arrays.asList("Bearer " + demoToken);
            headerMap.put("Authorization", tokenList);
            requestTemplate.headers(headerMap);
         }
    };
}

上面第一个方法是用于输出请求响应日志,如果需要自定义日志格式,需要自己继承feign.Logger类重写例如logRequest()logAndRebufferResponse()方法并注入到容器中。如果请求日志未输出,可能是由于日志级别问题导致,feign请求日志级别为debug,需要在application配置文件中进行如下配置:

#单独配置某个feignClient类的日志级别
logging.level.com.xxx.xxxClient=DEBUG
#配置整个包的日志级别
logging.level.com.xxx.service=DEBUG

第二个方法则是配置全局请求的header,需要实现RequestInterceptor接口实现apply()方法。例子为oath2标准添加token。

使用注解自定义header

@RequestMapping(value = "/getdemotoken", method = RequestMethod.GET)
DemoResp getDemoList(@RequestHeader("Authorization") String jwtToken);

使用@RequestHeader注解并标注key也是一种添加header的方式,但用这种方式添加的header会被上面全局配置的header覆盖,所以需要对全局配置部分作出修改:

@Bean
public RequestInterceptor getRequestInterceptor(){
    return new RequestInterceptor() {
        @Override
 public void apply(RequestTemplate requestTemplate) {
            if (!requestTemplate.headers().containsKey("Authorization")){
                Map<String, Collection<String>> headerMap = new HashMap<>();
                List<String> tokenList = Arrays.asList("Bearer "+tokenServer.getAccessToken());
                headerMap.put("Authorization", tokenList);
                requestTemplate.headers(headerMap);
             }
        }
    };
}

在设置header前判断目前headerMap是否存在相同的header信息,注意headers()方法取出的map是只读的,需要自己创建map来进行修改覆盖。

处理部分服务返回Content-Type为text-plain的情况

有些服务例如微信在返回json字符串时的格式并不是application-json,而是text-plain。此时如果使用对象接收可能会报错。
如果报错解决办法有两个,一是直接用字符串接收然后手动反序列化;二是通过在配置类中配置Decoder来处理text-plain格式。下面为Decoder配置方法:

/**
 * 自定义新的decoder来处理微信返回text plain的返回类型
 * @return
 */@Bean
public Decoder feignDecoder(){
    ObjectFactory<HttpMessageConverters> messageConverters = new ObjectFactory<HttpMessageConverters>() {
        @Override
     public HttpMessageConverters getObject() throws BeansException {
            return new HttpMessageConverters(new MappingJackson2HttpMessageConverter(){
                @Override
                public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) {
                    List<MediaType> myType = new ArrayList<>(supportedMediaTypes);
                    myType.add(MediaType.TEXT_PLAIN);
                    super.setSupportedMediaTypes(myType);
                  }
            });
          }
    };
   return new SpringDecoder(messageConverters);
}

guomz
16 声望1 粉丝

不求做完人,只求做凡人。