起因
因为公司提供的一个基础服务需要被其他多个项目调用,但其他项目注册中心不一致(eureka,nacos),feign调用需要指定url,且修改地方比较多
目的
手动创建feignClient,统一管理服务调用
服务调用发起方
- 服务A
package com.just.hungry.test.feign;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* @author lbh
* 可以使用feignclient指定地址,也可以手动封装feignClient的参数信息
*/
//@FeignClient(name = "服务名",url = "www.justHungry.cn")
public interface TestControllerA {
/**
* feign 测试
*
* @return success
*/
@RequestMapping(value = "/customize", method = RequestMethod.GET)
String queryCustomize();
}
- 服务B
package com.just.hungry.test.feign;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* @author lbh
*/
public interface TestControllerB {
/**
* feign 测试
*
* @return success
*/
@RequestMapping(value = "/customize", method = RequestMethod.GET)
String queryCustomize();
}
- 统一配置服务调用信息
package com.just.hungry.test.feign;
import com.just.base.feign.JustFeignClient;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author lbh
*/
@Configuration
@AutoConfigureAfter({JustFeignClient.class})
public class TestFeignClientConfig {
private final JustFeignClient justFeignClient;
public TestFeignClientConfig(JustFeignClient justFeignClient) {
this.justFeignClient = justFeignClient;
}
/**
* 自定义服务名和url进行feign的调用,feign支持服务名和url的请求,如果有url则使用httpclient请求,没有url则通过ribbon使用服务名发送请求
* 这里url和name可以使用properties的方式放在启动yaml文件中,name直接取${spring.application.name};也可以放在数据库中动态加载
*/
@Bean
@ConditionalOnMissingBean
public TestControllerA customizeFeignApiA() {
return justFeignClient.generateFeignClientV1("服务名", "www.justHungry.cn", TestControllerA.class);
}
@Bean
@ConditionalOnMissingBean
public TestControllerB customizeFeignApiB() {
return justFeignClient.generateFeignClientV2("服务名", "www.justHungry.cn", TestControllerB.class);
}
}
feign 实现
这里其实可以直接配置在发起方,拆开是为了做成jar供其他服务使用
- feign实现
package com.just.base.feign;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.Client;
import feign.Feign;
import feign.RequestInterceptor;
import feign.codec.Decoder;
import feign.codec.Encoder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.FeignClientBuilder;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import java.util.ArrayList;
import java.util.Map;
/**
* @author lbh
* <p>
* 如果使用generateFeignClientV2 则@Import(FeignClientsConfiguration.class)不需要添加
*/
@Configuration
@Import(FeignClientsConfiguration.class)
public class JustFeignClient {
private final ApplicationContext applicationContext;
public JustFeignClient(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
private final Encoder encoder = new SpringEncoder(() -> new HttpMessageConverters(new MappingJackson2HttpMessageConverter(new ObjectMapper())));
private final Decoder decoder = new SpringDecoder(() -> new HttpMessageConverters(new MappingJackson2HttpMessageConverter(new ObjectMapper())));
private final Client client = new Client.Default(null, null);
/**
* 适用于所有feign版本
*/
public <T> T generateFeignClientV1(String url, String name, Class<T> feignClass) {
Map<String, RequestInterceptor> map = applicationContext.getBeansOfType(RequestInterceptor.class);
return Feign.builder().contract(new SpringMvcContract())
.client(client)
.encoder(encoder)
.decoder(decoder)
.errorDecoder(new JustFeignErrorDecoder())
.requestInterceptors(new ArrayList<>(map.values()))
.target(feignClass, url == null ? name : url);
}
/**
* 适用于openfeign 2.x
*/
public <T> T generateFeignClientV2(String name, String url, Class<T> feignClass) {
FeignClientBuilder feignClientBuilder = new FeignClientBuilder(applicationContext);
FeignClientBuilder.Builder<T> builder = feignClientBuilder.forType(feignClass, name);
if (StringUtils.isNotBlank(url)) {
builder.url(url);
}
return builder.build();
}
}
- 自定义拦截器
package com.just.base.feign;
import com.alibaba.fastjson.JSON;
import feign.RequestInterceptor;
import feign.RequestTemplate;
/**
* @author lbh
* <p>
* 自定义的feign拦截器。服务A通过feign访问服务B时可以使用此拦截器做一些自定义功能实现,例如我们用来做服务间访问的鉴权
*/
public class JustFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
// TODO
}
}
- 注册配置bean
package com.just.base.feign;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author lbh
*/
@Configuration
public class JustFeignClientConfiguration {
private final ApplicationContext applicationContext;
public JustFeignClientConfiguration(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Bean
@ConditionalOnMissingBean
public JustFeignInterceptor acTokenRequestInterceptor() {
return new JustFeignInterceptor();
}
@Bean
@ConditionalOnMissingBean
public JustFeignClient justFeignClient() {
return new JustFeignClient(applicationContext);
}
}
- 自定义异常信息处理
public class JustFeignErrorDecoder implements ErrorDecoder {
private static final Logger logger = LoggerFactory.getLogger(JustFeignErrorDecoder.class);
@Override
public Exception decode(String methodKey, Response response) {
if (response.status() == JustFeignErrorDecoder.ERROR_STATUS) {
try {
logger.info("JustFeignErrorDecoder #decode request-method:{} request-url:{}", methodKey, JSON.toJSONString(response.request().url()));
InputStream is = response.body().asInputStream();
StringBuilder xmlMsg = new StringBuilder();
byte[] b = new byte[4096];
for (int n; (n = is.read(b)) != -1; ) {
xmlMsg.append(new String(b, 0, n, StandardCharsets.UTF_8));
}
logger.info("JustFeignErrorDecoder #decode response-inputStream:{}", xmlMsg);
ApiResponse api = JacksonUtils.fromJson(xmlMsg.toString(), ApiResponse.class);
throw new ServiceException(api.getCode(), api.getData().toString());
} catch (IOException ex) {
logger.error("JustFeignErrorDecoder decode ex:{}", ex.toString());
throw new ServiceException(SYSTEM_IO_ERROR_CODE, SYSTEM_IO_ERROR_MSG);
}
} else {
return new ErrorDecoder.Default().decode(methodKey, response);
}
}
}
添加配置类扫描
just\src\main\resources\META-INF\spring.factories 文件添加如下配置
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.just.base.feign.JustFeignClientConfiguration,\
com.just.hungry.test.feign.TestFeignClientConfig,\