feign定制使用
- 项目背景
- 基本模式和流程
- 引入OpenFeign
- Feign的定制
- Encoder(加签)
- RequestInterceptor(Header传递)
- Decoder(统一解码)
项目背景
公司原先的模式是给客户提供统一的功能; 但是需求这种事情无法满足所有客户的需求; 因为各行各业都很卷,客户感觉自己没有被重视,客户会撂挑子不干的呀,因此公司换了一种玩法: 功能对外开放, 客户你不是觉得你提的需求简单嘛, you can you up, no can no bibi
;
基本模式和流程
就是将现在的业务能力提供出去,对外暴露一个maven的依赖,客户的开发人员引入依赖就可以拥有默认的业务功能;
基本流程为: 前端调用(可以定制) → 客户服务(作为后端,主要面向一层) → 调用中台网关(新增的一层网关) → 内部服务
引入OpenFeign
- 公司内部服务于服务之间的调用使用的是OpenFeign
- 使用OpenFeign时代码足够简洁
- 公司开发人员使用OpenFeign贼溜(虽然他们不知道contextId是什么,碰到两个服务名相同的FeignClient就束手无策)
可能是基于上面的原因,也可能是项目时间紧,拍板子的人随便拍一下脑袋就决定了,那么使用OpenFeign这个事情就愉快的定下来了;
Feign的定制
但是使用过程中碰到了一些问题,因为以前对OpenFeign也有一定的了解,所以解决了使用过程中碰到的一些OpenFeign的问题;简单记录一下;
Encoder(加签)
接口对外暴露那么肯定会加签, 对接第三方的时候都需要有密钥/token之类的; 都是尽可能的辨识出调用方,防止被人恶意攻击
使用目的
使用FeignClient时,参数在调用的时候需要包一层,直接那实体类来看
代码语言:javascript复制public class AppBaseRequest {
private String ver;
private String partnerId;
private String appId;
//你以为你只是传的对象实际上只是传递对象内的一个属性字段
private String requestBody;
private String sign;
}
解决方案
由于头一次碰到传参的时候把参数改的体无完肤的,因此debug时取了个巧,可能不是好的方案,但是运行正常
代码语言:javascript复制@Configuration
@EnableConfigurationProperties(SignatureProperties.class)
public class AppletAutoConfiguration {
@Bean
public EdenSignature edenSignature(SignatureProperties signatureProperties) {
return new DefaultEdenSignature(signatureProperties);
}
//默认的SpringEncoder注入是有ConditionOnMissionBean; 有了这个默认的就没了
@Bean
public AppletBossEncoder feignEncoder(ObjectFactory<HttpMessageConverters> messageConverters, EdenSignature edenSignature){
return new AppletBossEncoder(messageConverters, edenSignature);
}
}
public class AppletBossEncoder extends SpringEncoder {
//加签抽了出来;如果RestTemplate调用也可以使用到
private final EdenSignature edenSignature;
public AppletBossEncoder(ObjectFactory<HttpMessageConverters> messageConverters,
EdenSignature edenSignature) {
super(messageConverters);
this.edenSignature = edenSignature;
}
@Override
public void encode(Object requestBody, Type bodyType, RequestTemplate request) throws EncodeException {
if (!AppBaseRequest.class.getName().equals(bodyType.getTypeName())) {
//requestBody原先的类型是 T ; 加签后就一定成了AppBaseRequest
requestBody = edenSignature.sign(requestBody);
bodyType = requestBody.getClass();
}
super.encode(requestBody, bodyType, request);
}
}
RequestInterceptor(Header传递)
由于原先前端很多信息是放到header里面传递的; 多了一层服务,那么header就可能丢失; 因为ServletRequest是有存放在一个本地线程变量中; 那么我们就取出来往后面传就行了
代码很简单; 但是有一个细节需要了解下: Feign在初始化的时候首先找自己的上下文的Config ; 如果有则使用自己的,如果没有就招parent上下文的(父上下文就是全局的配置) ; 那么下面的代码就一定要放在一个全局的配置中; 即: 这个Bean所在的类上需要标记@Configuration
代码语言:javascript复制 @Bean
public RequestInterceptor headerPassInterceptor() {
return requestTemplate -> {
HttpServletRequest servletRequest = RequestUtils.currentServletRequest();
//允许返回null ; 防止本次feign调用并不是前端传递过来的; 比如跑一个定时任务,根本就没有上下文的ServletRequest
if (servletRequest != null) {
Enumeration<String> headerNames = servletRequest.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
if (!EXCLUDE_HEADER.contains(headerName)) {
Enumeration<String> headValue = servletRequest.getHeaders(headerName);
requestTemplate.header(headerName, Collections.list(headValue));
}
}
}
};
}
Decoder(统一解码)
这个是以前项目就碰到过的一个问题;我们对接了有十几个服务,他们的返回信息很多都不一样; 比如
- 错误码有code,statusCode,status;
- 返回实体类有result,data
- 返回错误消息有message,有errorMessage
为此,
- 老的项目每次执行远程调用就要在业务代码里面判断成功了嘛? 成功了再取出实体类,
- 新的项目抽了一层,包名各有不同,就是为了处理这个判断,只返回成功的实体对象
叔可忍,婶婶不能忍; 刚好看过解码的代码,改一下试试:
希望的效果
对接了很多的服务; 但是我都可以使用一个Result<T>接受,使用起来岂不更舒服.(实际一写代码,发现还可以更牛逼一点)
统一的代码抽离
因为每一个FeignClient都可能有不同的返回值,因此一个Decoder走不遍填下
代码语言:javascript复制public abstract class AbstractCustomDecoder<T> extends Decoder.Default {
protected static final String DEFAULT_SUCCESS_CODE = "200";
private final Class<T> tClass;
protected AbstractCustomDecoder() {
tClass = retrieveAnnotationType();
if (tClass == null || ResponseEntity.class.equals(tClass)) {
throw new EdenAbstractException();
}
}
@Override
public Object decode(Response response, Type type) throws IOException {
return customDecoder(response, type);
}
protected Object customDecoder(Response response, Type type) throws IOException {
Class<?> rawClass = ResolvableType.forType(type).getRawClass();
String body = Util.toString(response.body().asReader());
T obj = JSONObject.parseObject(body, tClass);
if (rawClass.equals(tClass)) {
return obj;
}
Object data = getData(obj).get();
if (type instanceof ParameterizedTypeImpl) {
ParameterizedTypeImpl parameterizedType = (ParameterizedTypeImpl) type;
if (parameterizedType.getActualTypeArguments() != null) {
Type typeArgument = parameterizedType.getActualTypeArguments()[0];
data = JSONObject.parseObject(JSONObject.toJSONString(getData(obj).get()), typeArgument);
}
}
if (String.class.equals(type)) {
return String.valueOf(data);
}
String code = getCode(obj).get();
String message = getMessage(obj).get();
Boolean status = getStatus(obj).get();
return convertResult(rawClass, message, data, status, code);
}
protected Object convertResult(Class<?> rawClass, String message, Object data, Boolean status,String code) {
if (Result.class.equals(rawClass)) {
return new Result<>(status, message, data, code);
}
return JSONObject.parseObject(JSONObject.toJSONString(data), rawClass);
}
protected abstract Supplier<Object> getData(T obj);
protected abstract Supplier<String> getCode(T obj);
protected abstract Supplier<String> getMessage(T obj);
protected Supplier<Boolean> getStatus(T obj) {
return () -> {
String code = getCode(obj).get();
return DEFAULT_SUCCESS_CODE.equals(code);
};
}
/**
* 检索泛型对应的实际类型
* @return
*/
private Class<T> retrieveAnnotationType(){
Type type = getClass().getGenericSuperclass();
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
for (Type actualTypeArgument : parameterizedType.getActualTypeArguments()) {
if (actualTypeArgument instanceof Class) {
return (Class) actualTypeArgument;
}
}
}
return null;
}
}
Demo的config
一定要注意,这里的方法上有@Bean;但是类上并没有@Configuration; 这样子就谁用谁配置
代码语言:javascript复制public class CustomDecoderConfig {
@Data
public static class BaiduResult<T>{
private String status;
private T result;
}
@Bean
public Decoder decoder() {
return new AbstractCustomDecoder<BaiduResult>() {
@Override
protected Supplier<Object> getData(BaiduResult obj) {
return obj::getResult;
}
@Override
protected Supplier<String> getCode(BaiduResult obj) {
return obj::getStatus;
}
@Override
protected Supplier<String> getMessage(BaiduResult obj) {
return () -> JSONObject.toJSONString(obj.getResult());
}
@Override
protected Supplier<Boolean> getStatus(BaiduResult obj) {
return () -> "OK".equals(obj.getStatus());
}
@Override
protected Object convertResult(Class<?> rawClass, String message, Object data, Boolean status, String code) {
if (BaiduResult.class.equals(rawClass)) {
BaiduResult cityResponse = new BaiduResult();
cityResponse.setStatus(code);
cityResponse.setResult(data);
return cityResponse;
}
return super.convertResult(rawClass, message, data, status, code);
}
};
}
}
效果展示
代码语言:javascript复制@EnableFeignClients(clients = AlaBossDecoderExample.BaiduLbsClient.class)
public class AlaBossDecoderExample {
@FeignClient(contextId = "demo.baiduLbs",
name = "baiduLbs",
url = "http://api.map.baidu.com",
configuration = CustomDecoderConfig.class)
public interface BaiduLbsClient {
/**
* 原生写法
*/
@GetMapping("/geocoder")
CustomDecoderConfig.BaiduResult<LocationInfo> resultGeocoder(@RequestParam("location") String location, @RequestParam("output") String output);
/**
* String 接受
*/
@GetMapping("/geocoder")
String stringGeocoder(@RequestParam("location") String location, @RequestParam("output") String output);
/**
* 只接受Data内部需要的实体对象
*/
@GetMapping("/geocoder")
LocationInfo dataGeocoder(@RequestParam("location") String location, @RequestParam("output") String output);
/**
* 标准接受方式
*/
@GetMapping("/geocoder")
Result<LocationInfo> standardGeocoder(@RequestParam("location") String location, @RequestParam("output") String output);
}
@Test
public void test() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(AlaBossDecoderExample.class,
FeignAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class
);
context.refresh();
AlaBossDecoderExample.BaiduLbsClient lbsClient = context.getBean(AlaBossDecoderExample.BaiduLbsClient.class);
String rest = lbsClient.stringGeocoder("39.983424,116.322987", "json");
System.out.println("rest = " rest);
LocationInfo json = lbsClient.dataGeocoder("39.983424,116.322987", "json");
System.out.println("json = " json);
CustomDecoderConfig.BaiduResult<LocationInfo> json1 = lbsClient.resultGeocoder("39.983424,116.322987", "json");
System.out.println("json1 = " json1);
Result<LocationInfo> json2 = lbsClient.standardGeocoder("39.983424,116.322987", "json");
System.out.println("json2 = " ToStringBuilder.reflectionToString(json2));
context.close();
}
}
展示下效果
实际超出了预期,被玩出新玩法了,随便接受….
- 可以实体接受
- 原先的数据格式可以接受
- 指定的数据格式可以寄售
- String也可以接受
rest = {"formatted_address":"北京市海淀区中关村大街27号1101-08室","business":"中关村,人民大学,苏州街","cityCode":131,"location":{"lng":116.322987,"lat":39.983424},"addressComponent":{"distance":"7","province":"北京市","city":"北京市","street":"中关村大街","district":"海淀区","street_number":"27号1101-08室","direction":"near"}}
json = AlaBossDecoderExample.LocationInfo(location=AlaBossDecoderExample.LocationInfo.Location(lng=116.322987, lat=39.983424), formatted_address=北京市海淀区中关村大街27号1101-08室, business=中关村,人民大学,苏州街, cityCode=131, addressComponent=AlaBossDecoderExample.LocationInfo.AddressComponent(city=北京市, direction=附近, distance=7, district=海淀区, province=北京市, street=中关村大街, street_number=27号1101-08室))
json1 = CustomDecoderConfig.BaiduResult(status=OK, result={"formatted_address":"北京市海淀区中关村大街27号1101-08室","business":"中关村,人民大学,苏州街","cityCode":131,"location":{"lng":116.322987,"lat":39.983424},"addressComponent":{"distance":"7","province":"北京市","city":"北京市","street":"中关村大街","district":"海淀区","street_number":"27号1101-08室","direction":"near"}})
json2 = com.freemud.eden.common.Result@35d6ca49[status=true,message={"formatted_address":"北京市海淀区中关村大街27号1101-08室","business":"中关村,人民大学,苏州街","cityCode":131,"location":{"lng":116.322987,"lat":39.983424},"addressComponent":{"distance":"7","province":"北京市","city":"北京市","street":"中关村大街","district":"海淀区","street_number":"27号1101-08室","direction":"附近"}},result={"formatted_address":"北京市海淀区中关村大街27号1101-08室","business":"中关村,人民大学,苏州街","cityCode":131,"location":{"lng":116.322987,"lat":39.983424},"addressComponent":{"distance":"7","province":"北京市","city":"北京市","street":"中关村大街","district":"海淀区","street_number":"27号1101-08室","direction":"附近"}},statusCode=OK]