Feign 请求动态URL
注意事项
- FeignClient 中不要写url, 使用 @RequestLine修饰方法
- 调用地方必须引入 FeignClientConfiguration, 必须有Decoder, Encoder
- 调用类必须以构建函数(Constructor) 的方式注入 FeignClient 类
- 传入URL作为参数;
代码如下:
FeignClient类:
@CompileStatic @FeignClient(name = "xxxxClient") public interface XxxFeignClient { @RequestLine("POST") ResponseDto notifySomething(URI baseUri, ApproveNotifyDto notifyDto); /** * * @param uri * @param queryMap: {userId: userId} * @return */ @RequestLine("GET") ResponseDto getSomething(URI baseUri, @QueryMap MapqueryMap) }
ClientCaller类:
@CompileStatic @Slf4j @Component @Import(FeignClientsConfiguration.class) public class CallerService { private XxxFeignClient xxxFeignClient @Autowired public CallerService(Decoder decoder, Encoder encoder) { xxxFeignClient = Feign.builder() //.client(client) .encoder(encoder) .decoder(decoder) .target(Target.EmptyTarget.create(XxxFeignClient.class)) } public ResponseDto notifySomething(String url, XxxxDto dto) { return xxxFeignClient.notifySomething(URI.create(url), dto) } /** * @param url: http://localhost:9104/ * @param userId */ public String getSomething(String url, String userId) { return xxxFeignClient.getSomething(URI.create(url), ["userId": userId]) } }
Feign重写URL以及RequestMapping
背景
由于项目采用的是 消费层 + API层(FeignClient) +服务层 的方式。
导致原项目有太多代码冗余复制的地方。例如FeignClient上要写@RequestMapping,同时要写请求路径,消费层还要写上RequestBody等等,比较麻烦。遂改进了一下方案,简化日常代码开发的工作量
场景
项目的架构是微服务架构,SpringBoot与SpringCloud均为原生版本。
效果展示
feign层无需写RequestMapping以及RequestBody或者RequestParam等
public interface UserDemoApi { /** * 新增用户 * @param userDemo 用户实体 * @return 用户信息 */ ApiResponseadd(UserDemo userDemo); /** * 获取用户信息 * @param userId 用户id * @param username 用户名 * @return 用户信息 */ ApiResponse findByUserIdAndUsername(String userId,String username); /** * 用户列表 * @param reqVo 条件查询 * @return 用户列表 */ ApiResponse > findByCondition(UserDemoReqVo reqVo); }
@FeignClient(value = "${api.feign.method.value}",fallback = UserDemoFeignApiFallbackImpl.class) public interface UserDemoFeignApi extends UserDemoApi { }
整体思路
- 首先要拆成两部分处理,一部分是服务端层,另一部分是客户端层
- 服务端层:主要做的事情就是注册RequestMapping.由于我们的api上是没有写任何注解的,所以我们需要在项目启动的时候把api上的方法都注册上去。另外一个要做的就是,由于我们不写RequestBody,所以要做参数解析配置
- 客户端层:主要做的事情是,项目启动的时候,feign会扫描api上的方法,在它扫描的同时,直接把url定下来存入RequestTemplate中。这样即使后续feign在执行apply没有路径时也不影响feign的正常请求。
实现
Feign的服务端层
1. 继承RequestMappingHandlerMapping,重写getMappingForMethod
- METHOD_MAPPING 为自定义的路径(即RequestMapping的value值)
- 在服务层的实现类中,打上自定义注解@CustomerAnnotation,只有有该注解的实现类才重写RequestMapping。
- 自定义的路径采用的是 类名+方法名
重写的内容
public class CustomerRequestMapping extends RequestMappingHandlerMapping { private final static String METHOD_MAPPING = "/remote/%s/%s"; @Override protected RequestMappingInfo getMappingForMethod(Method method, Class> handlerType) { RequestMappingInfo requestMappingInfo = super.getMappingForMethod(method,handlerType); if(requestMappingInfo!=null){ if(handlerType.isAnnotationPresent(CustomerAnnotation.class)){ if(requestMappingInfo.getPatternsCondition().getPatterns().isEmpty()|| requestMappingInfo.getPatternsCondition().getPatterns().contains("")){ String[] path = getMethodPath(method, handlerType); requestMappingInfo = RequestMappingInfo.paths(path).build().combine(requestMappingInfo); } } }else{ if(handlerType.isAnnotationPresent(CustomerAnnotation.class)){ String[] path = getMethodPath(method, handlerType); requestMappingInfo = RequestMappingInfo.paths(path).methods(RequestMethod.POST).build(); } } return requestMappingInfo; } private String[] getMethodPath(Method method, Class> handlerType) { Class>[] interfaces = handlerType.getInterfaces(); String methodClassName = interfaces[0].getSimpleName(); methodClassName = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, methodClassName); String[] path = {String.format(METHOD_MAPPING,methodClassName, method.getName())}; return path; }
覆盖RequestMappingHandlerMapping
@Configuration @Order(Ordered.HIGHEST_PRECEDENCE) public class VersionControlWebMvcConfiguration implements WebMvcRegistrations { @Override public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { return new CustomerRequestMapping(); } }
2. 服务层Controller
- 实现api接口(即Feign层的接口)
- 打上自定义的标识注解@CustomerAnnotation
@RestController @CustomerAnnotation @RequestMapping public class UserDemoController implements UserDemoFeignApi { private final static Logger logger = LoggerFactory.getLogger(UserDemoController.class); @Resource private UserDemoService userDemoService; @Override public ApiResponseadd(UserDemo userDemo) { logger.info("request data:<{}>",userDemo); return userDemoService.add(userDemo); } @Override public ApiResponse findByUserIdAndUsername(String userId,String username) { logger.info("request data:<{}>",userId+":"+username); return ApiResponse.success(new UserDemo()); } @Override public ApiResponse > findByCondition(UserDemoReqVo reqVo) { logger.info("request data:<{}>",reqVo); return userDemoService.findByCondition(reqVo); } }
自此,Fiegn的服务端的配置已经配置完毕。现在我们来配置Feign的客户端
Feign的客户端配置
重写Feign的上下文SpringMvcContract
核心代码为重写processAnnotationOnClass的内容。
- 从MethodMetadata 对象中获取到RequestTemplate,后续的所有操作都是针对于这个
- 获取到类名以及方法名,作为RequestTemplate对象中url的值,此处应与服务层的配置的URL路径一致
- 默认请求方式都为POST
特殊情况处理:在正常情况下,多参数且没有@RequestParams参数注解的情况下,Feign会直接抛异常且终止启动。所以需要对多参数做额外处理
- 判断当前方法的参数数量,如果不超过2个不做任何处理
- 对于超过2个参数的方法,需要对其做限制。首先,方法必须满足命名规范,即类似findByUserIdAndUsername。以By为起始,And作为连接。
- 截取并获取参数名称
- 将名称按顺序存入RequestTemplate对象中的querie属性中。同时,要记得MethodMetadata 对象中的indexToName也需要存入信息。Map
map,key为参数的位置(从0开始),value为参数的名称
@Component public class CustomerContact extends SpringMvcContract { private final static Logger logger = LoggerFactory.getLogger(CustomerContact.class); private final static String METHOD_PATTERN_BY = "By"; private final static String METHOD_PATTERN_AND = "And"; private final MapprocessedMethods = new HashMap<>(); private final static String METHOD_MAPPING = "/remote/%s/%s"; private Map parameterIndexMap = new ConcurrentHashMap<>(100); public CustomerContact() { this(Collections.emptyList()); } public CustomerContact(List annotatedParameterProcessors) { this(annotatedParameterProcessors,new DefaultConversionService()); } public CustomerContact(List annotatedParameterProcessors, ConversionService conversionService) { super(annotatedParameterProcessors, conversionService); } /** * 重写URL * @param data 类名以及方法名信息 * @param clz api类 */ @Override protected void processAnnotationOnClass(MethodMetadata data, Class> clz) { RequestTemplate template = data.template(); String configKey = data.configKey(); if(StringUtils.isBlank(template.url())){ template.append(getTemplatePath(configKey)); template.method(RequestMethod.POST.name()); } // 构造查询条件 templateQuery(template,data); super.processAnnotationOnClass(data, clz); } /** * @param template 请求模板 */ private void templateQuery(RequestTemplate template,MethodMetadata data){ try{ String configKey = data.configKey(); if(manyParameters(data.configKey())){ Method method = processedMethods.get(configKey); String methodName = method.getName(); String key = getTemplatePath(configKey); Integer parameterIndex = 0; if(parameterIndexMap.containsKey(key)){ parameterIndexMap.put(key,parameterIndex++); }else{ parameterIndexMap.put(key,parameterIndex); } int index = methodName.indexOf(METHOD_PATTERN_BY); if(index>=0){ String[] parametersName = methodName.substring(index+METHOD_PATTERN_BY.length()).split(METHOD_PATTERN_AND); String parameterName = parametersName[parameterIndex]; String caseName = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, parameterName); Collection param = addTemplatedParam(template.queries().get(parameterName), caseName); template.query(caseName,param); setNameParam(data,caseName,parameterIndex); } } }catch (Exception e){ e.printStackTrace(); logger.error("template construct query failed:<{}>",e.getMessage()); } } /** * 构造url路径 * @param configKey 类名#方法名信息 * @return URL路径 */ private String getTemplatePath(String configKey) { Method method = processedMethods.get(configKey); int first = configKey.indexOf("#"); String apiName = configKey.substring(0,first); String methodClassName = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, apiName); return String.format(METHOD_MAPPING,methodClassName,method.getName()); } @Override public MethodMetadata parseAndValidateMetadata(Class> targetType, Method method) { this.processedMethods.put(Feign.configKey(targetType, method), method); return super.parseAndValidateMetadata(targetType,method); } @Override protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) { if(manyParameters(data.configKey())){ return true; } return super.processAnnotationsOnParameter(data,annotations,paramIndex); } private void setNameParam(MethodMetadata data, String name, int i) { Collection names = data.indexToName().containsKey(i) ? data.indexToName().get(i) : new ArrayList (); names.add(name); data.indexToName().put(i, names); } /** * * 多参数校验 * @param configKey 类名#方法名 * @return 参数是否为1个以上 */ private boolean manyParameters(String configKey){ Method method = processedMethods.get(configKey); return method.getParameterTypes().length > 1; }
最后还有一处修改
由于我们在方法上没有写上RequestBody注解,所以此处需要进行额外的处理
- 只针对于带有FeignClient的实现类才做特殊处理
- 如果入参为非自定义对象,即为基本数据类型,则直接返回即可
- 自定义对象,json转换后再返回
@Configuration public class CustomerArgumentResolvers implements HandlerMethodArgumentResolver { // 基本数据类型 private static final Class[] BASE_TYPE = new Class[]{String.class,int.class,Integer.class,boolean.class,Boolean.class, MultipartFile.class}; @Override public boolean supportsParameter(MethodParameter parameter) { //springcloud的接口入参没有写@RequestBody,并且是自定义类型对象 也按JSON解析 if (AnnotatedElementUtils.hasAnnotation(parameter.getContainingClass(), FeignClient.class)) { if(parameter.getExecutable().getParameters().length<=1){ return true; } } return false; } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception { final Type type = parameter.getGenericParameterType(); String parameters = getParameters(nativeWebRequest); if(applyType(type)){ return parameters; }else { return JSON.parseObject(parameters,type); } } private String getParameters(NativeWebRequest nativeWebRequest) throws Exception{ HttpServletRequest servletRequest = nativeWebRequest.getNativeRequest(HttpServletRequest.class); String jsonBody = ""; if(servletRequest!=null){ ServletInputStream inputStream = servletRequest.getInputStream(); jsonBody = IOUtils.toString(inputStream); } return jsonBody; } private boolean applyType(Type type){ for (Class classType : BASE_TYPE) { if(type.equals(classType)){ return true; } } return false; } }
@Configuration public class CustomerConfigAdapter implements WebMvcConfigurer { @Resource private CustomerArgumentResolvers customerArgumentResolvers; @Override public void addArgumentResolvers(Listresolvers) { resolvers.add(customerArgumentResolvers); } }
以上就是配置的所有内容,整体代码量很少。
但可能需要读下源码才能理解
这些仅为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。