Feign?請(qǐng)求動(dòng)態(tài)URL方式
Feign 請(qǐng)求動(dòng)態(tài)URL
注意事項(xiàng)
- FeignClient 中不要寫url, 使用 @RequestLine修飾方法
- 調(diào)用地方必須引入 FeignClientConfiguration, 必須有Decoder, Encoder
- 調(diào)用類必須以構(gòu)建函數(shù)(Constructor) 的方式注入 FeignClient 類
- 傳入U(xiǎn)RL作為參數(shù);
代碼如下:
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 Map<String, String> queryMap) ?? }
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
背景
由于項(xiàng)目采用的是 消費(fèi)層 + API層(FeignClient) +服務(wù)層 的方式。
導(dǎo)致原項(xiàng)目有太多代碼冗余復(fù)制的地方。例如FeignClient上要寫@RequestMapping,同時(shí)要寫請(qǐng)求路徑,消費(fèi)層還要寫上RequestBody等等,比較麻煩。遂改進(jìn)了一下方案,簡化日常代碼開發(fā)的工作量
場(chǎng)景
項(xiàng)目的架構(gòu)是微服務(wù)架構(gòu),SpringBoot與SpringCloud均為原生版本。
效果展示
feign層無需寫RequestMapping以及RequestBody或者RequestParam等
public interface UserDemoApi { ? ? /** ? ? ?* 新增用戶 ? ? ?* @param userDemo 用戶實(shí)體 ? ? ?* @return 用戶信息 ? ? ?*/ ? ? ApiResponse<UserDemo> add(UserDemo userDemo); ? ? /** ? ? ?* 獲取用戶信息 ? ? ?* @param userId 用戶id ? ? ?* @param username 用戶名 ? ? ?* @return 用戶信息 ? ? ?*/ ? ? ApiResponse<UserDemo> findByUserIdAndUsername(String userId,String username); ? ? /** ? ? ?* 用戶列表 ? ? ?* @param reqVo 條件查詢 ? ? ?* @return 用戶列表 ? ? ?*/ ? ? ApiResponse<PageResult<UserDemo>> findByCondition(UserDemoReqVo reqVo); }
@FeignClient(value = "${api.feign.method.value}",fallback = UserDemoFeignApiFallbackImpl.class) public interface UserDemoFeignApi extends UserDemoApi { }
整體思路
- 首先要拆成兩部分處理,一部分是服務(wù)端層,另一部分是客戶端層
- 服務(wù)端層:主要做的事情就是注冊(cè)RequestMapping.由于我們的api上是沒有寫任何注解的,所以我們需要在項(xiàng)目啟動(dòng)的時(shí)候把a(bǔ)pi上的方法都注冊(cè)上去。另外一個(gè)要做的就是,由于我們不寫RequestBody,所以要做參數(shù)解析配置
- 客戶端層:主要做的事情是,項(xiàng)目啟動(dòng)的時(shí)候,feign會(huì)掃描api上的方法,在它掃描的同時(shí),直接把url定下來存入RequestTemplate中。這樣即使后續(xù)feign在執(zhí)行apply沒有路徑時(shí)也不影響feign的正常請(qǐng)求。
實(shí)現(xiàn)
Feign的服務(wù)端層
1. 繼承RequestMappingHandlerMapping,重寫getMappingForMethod
- METHOD_MAPPING 為自定義的路徑(即RequestMapping的value值)
- 在服務(wù)層的實(shí)現(xiàn)類中,打上自定義注解@CustomerAnnotation,只有有該注解的實(shí)現(xiàn)類才重寫RequestMapping。
- 自定義的路徑采用的是 類名+方法名
重寫的內(nèi)容
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. 服務(wù)層Controller
- 實(shí)現(xiàn)api接口(即Feign層的接口)
- 打上自定義的標(biāo)識(shí)注解@CustomerAnnotation
@RestController @CustomerAnnotation @RequestMapping public class UserDemoController implements UserDemoFeignApi { ? ? private final static Logger logger = LoggerFactory.getLogger(UserDemoController.class); ? ? @Resource ? ? private UserDemoService userDemoService; ? ? @Override ? ? public ApiResponse<UserDemo> add(UserDemo userDemo) { ? ? ? ? logger.info("request data:<{}>",userDemo); ? ? ? ? return userDemoService.add(userDemo); ? ? } ? ? @Override ? ? public ApiResponse<UserDemo> findByUserIdAndUsername(String userId,String username) { ? ? ? ? logger.info("request data:<{}>",userId+":"+username); ? ? ? ? return ApiResponse.success(new UserDemo()); ? ? } ? ? @Override ? ? public ApiResponse<PageResult<UserDemo>> findByCondition(UserDemoReqVo reqVo) { ? ? ? ? logger.info("request data:<{}>",reqVo); ? ? ? ? return userDemoService.findByCondition(reqVo); ? ? } }
自此,F(xiàn)iegn的服務(wù)端的配置已經(jīng)配置完畢?,F(xiàn)在我們來配置Feign的客戶端
Feign的客戶端配置
重寫Feign的上下文SpringMvcContract
核心代碼為重寫processAnnotationOnClass的內(nèi)容。
- 從MethodMetadata 對(duì)象中獲取到RequestTemplate,后續(xù)的所有操作都是針對(duì)于這個(gè)
- 獲取到類名以及方法名,作為RequestTemplate對(duì)象中url的值,此處應(yīng)與服務(wù)層的配置的URL路徑一致
- 默認(rèn)請(qǐng)求方式都為POST
特殊情況處理:在正常情況下,多參數(shù)且沒有@RequestParams參數(shù)注解的情況下,F(xiàn)eign會(huì)直接拋異常且終止啟動(dòng)。所以需要對(duì)多參數(shù)做額外處理
- 判斷當(dāng)前方法的參數(shù)數(shù)量,如果不超過2個(gè)不做任何處理
- 對(duì)于超過2個(gè)參數(shù)的方法,需要對(duì)其做限制。首先,方法必須滿足命名規(guī)范,即類似findByUserIdAndUsername。以By為起始,And作為連接。
- 截取并獲取參數(shù)名稱
- 將名稱按順序存入RequestTemplate對(duì)象中的querie屬性中。同時(shí),要記得MethodMetadata 對(duì)象中的indexToName也需要存入信息。Map<Integer,Collection> map,key為參數(shù)的位置(從0開始),value為參數(shù)的名稱
@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 Map<String, Method> processedMethods = new HashMap<>(); ? ? private final static String METHOD_MAPPING = "/remote/%s/%s"; ? ? private Map<String, Integer> parameterIndexMap = new ConcurrentHashMap<>(100); ? ? public CustomerContact() { ? ? ? ? this(Collections.emptyList()); ? ? } ? ? public CustomerContact(List<AnnotatedParameterProcessor> annotatedParameterProcessors) { ? ? ? ? this(annotatedParameterProcessors,new DefaultConversionService()); ? ? } ? ? public CustomerContact(List<AnnotatedParameterProcessor> 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()); ? ? ? ? } ? ? ? ? // 構(gòu)造查詢條件 ? ? ? ? templateQuery(template,data); ? ? ? ? super.processAnnotationOnClass(data, clz); ? ? } ? ? /** ? ? ?* @param template 請(qǐng)求模板 ? ? ?*/ ? ? 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<String> 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()); ? ? ? ? } ? ? } ? ? /** ? ? ?* 構(gòu)造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<String> ? ? ? ? ? ? ? ? names = ? ? ? ? ? ? ? ? data.indexToName().containsKey(i) ? data.indexToName().get(i) : new ArrayList<String>(); ? ? ? ? names.add(name); ? ? ? ? data.indexToName().put(i, names); ? ? } ? ? /** ? ? ?* ? ? ?* 多參數(shù)校驗(yàn) ? ? ?* @param configKey 類名#方法名 ? ? ?* @return 參數(shù)是否為1個(gè)以上 ? ? ?*/ ? ? private boolean manyParameters(String configKey){ ? ? ? ? Method method = processedMethods.get(configKey); ? ? ? ? return method.getParameterTypes().length > 1; ? ? }
最后還有一處修改
由于我們?cè)诜椒ㄉ蠜]有寫上RequestBody注解,所以此處需要進(jìn)行額外的處理
- 只針對(duì)于帶有FeignClient的實(shí)現(xiàn)類才做特殊處理
- 如果入?yún)榉亲远x對(duì)象,即為基本數(shù)據(jù)類型,則直接返回即可
- 自定義對(duì)象,json轉(zhuǎn)換后再返回
@Configuration public class CustomerArgumentResolvers implements HandlerMethodArgumentResolver { ? ? // 基本數(shù)據(jù)類型 ? ? 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的接口入?yún)]有寫@RequestBody,并且是自定義類型對(duì)象 也按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(List<HandlerMethodArgumentResolver> resolvers) { ? ? ? ? resolvers.add(customerArgumentResolvers); ? ? } }
以上就是配置的所有內(nèi)容,整體代碼量很少。
但可能需要讀下源碼才能理解
這些僅為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java使用CountDownLatch實(shí)現(xiàn)網(wǎng)絡(luò)同步請(qǐng)求的示例代碼
CountDownLatch 是一個(gè)同步工具類,用來協(xié)調(diào)多個(gè)線程之間的同步,它能夠使一個(gè)線程在等待另外一些線程完成各自工作之后,再繼續(xù)執(zhí)行。被將利用CountDownLatch實(shí)現(xiàn)網(wǎng)絡(luò)同步請(qǐng)求,異步同時(shí)獲取商品信息組裝,感興趣的可以了解一下2023-01-01Maven在Windows中的配置以及IDE中的項(xiàng)目創(chuàng)建實(shí)例
下面小編就為大家?guī)硪黄狹aven在Windows中的配置以及IDE中的項(xiàng)目創(chuàng)建實(shí)例。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-09-09Java實(shí)現(xiàn)DES加密與解密,md5加密以及Java實(shí)現(xiàn)MD5加密解密類
這篇文章主要介紹了Java實(shí)現(xiàn)DES加密與解密,md5加密以及Java實(shí)現(xiàn)MD5加密解密類 ,需要的朋友可以參考下2015-11-11Java SpringBoot自動(dòng)裝配原理詳解及源碼注釋
SpringBoot的自動(dòng)裝配是拆箱即用的基礎(chǔ),也是微服務(wù)化的前提。其實(shí)它并不那么神秘,我在這之前已經(jīng)寫過最基本的實(shí)現(xiàn)了,大家可以參考這篇文章,來看看它是怎么樣實(shí)現(xiàn)的,我們透過源代碼來把握自動(dòng)裝配的來龍去脈2021-10-10詳解Spring Security 中的四種權(quán)限控制方式
這篇文章主要介紹了詳解Spring Security 中的四種權(quán)限控制方式,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10Oracle+Mybatis的foreach insert批量插入報(bào)錯(cuò)的快速解決辦法
本文給大家介紹Oracle+Mybatis的foreach insert批量插入報(bào)錯(cuò)的快速解決辦法,非常不錯(cuò),具有參考借鑒價(jià)值,感興趣的朋友參考下吧2016-08-08