前言 微服务开发时必定涉及到服务注册、服务发现及服务间的通信,一般而言,一个大型系统被拆分为多个小服务,则服务之间使用 dubbo 以 RPC 方式通信,会更快、更轻量、并发高,而不同系统之间的远程调用可以使用 feign 的以 HTTP 的方式更适合,而无论是用哪种方式都绕不开服务注册与发现。本文中我们将使用 openfeign 来作为通信方式,使用 Nacos 完成服务注册与发现,另外将介绍 Spring 入参绑定。
一、SrpingBoot 集成 openfeign
启动 nacos
使用 docker-compose 方式部署单机 nacos,docker-compose.yml 文件如下:
1 2 3 4 5 6 7 8 9 10 version: '3' services: nacos: image: nacos/nacos-server:v2.0.4 container_name: nacos restart: always environment: MODE: standalone ports: - 8848 :8848
然后使用 docker-compose up -d 启动 nacos。
创建 Springboot 工程
创建两个Springboot工程 feign-service-a 、feign-service-b。
注意:pom.xml 文件中需引入以下依赖,以支持服务名调用,实现负载均衡。
1 2 3 4 5 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-loadbalancer</artifactId > <version > 2.2.9.RELEASE</version > </dependency >
使用示例:
1 2 @GetMapping("/hello") R hello (@RequestParam String name) ;
注意:@RequestParam 注解必须添加,否则 name 参数会被放到 body 中,feign 自动将 GET 请求转化为 POST 请求造成错误。
1 2 @GetMapping("/query") R query (@SpringQueryMap User user) ;
注意:参数为对象时必须添加 @SpringQueryMap 注解才能正确解析参数。
1 2 @PostMapping("/create") R create (@RequestBody User user) ;
1 2 @DeleteMapping("/{id}") R delete (@PathVariable Integer id) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RequestMapping("/a") @FeignClient(name = "dynamicURLFeign", url = "http://127.0.0.1:8888") public interface DynamicURLFeign { @GetMapping("/dynamic") R dynamic (URI uri) ; }
1 2 3 4 5 @SneakyThrows @GetMapping("/dynamic") public R dynamic () { return dynamicURLFeign.dynamic(new URI ("http://127.0.0.1:8081" )); }
注意:@FeignClient 注解中,name 是必须的,若未指定 url 则会从注册中心寻找和 name 值同名的服务,但当我们需要调用的服务并未在注册中心上时,就需要指定 url 的值了。url 的值可以是固定值,也可以使用 EL 表示式来从配置中读取,如:name = "${feign.url}",若此处 url 的值需要根据业务动态变化,此时配置的 url 值已经不重要了,但必须得是合法的 url 格式。此时只需要在 feign 调用的方法中加上 URI 参数即可向指定的地址发起请求。 我们可以看到这种方式其实有几个缺点:
feign 调用的方法中需要传与方法无关的参数 URI,并且每个方法中都需要写,很麻烦;
URI 会抛出受查异常,使得必须去处理这种异常,不够美观;
拦截器中 template.path 或 template.url 取到的就不是 mapping 路由,而是整个 url 链接,不方便进行路由判断;
因此,我发现了下面这种的方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public void apply (RequestTemplate template) { template.header("auth" , "feign-service-b" ); Target<?> target = template.feignTarget(); if ("/a/dynamic1" .equals(template.path())) { template.target("http://127.0.0.1:8081" ); target = new Target .HardCodedTarget<>(target.type(), target.name(), target.url()); template.feignTarget(target); } }
在 feign 拦截器中,设置 template.target() 参数即可,feign 方法调用无需做任何修改,但 @FeignClient 中还是需要加 url 参数哦。
二、Spring 入参绑定 前后端分离开发中,前端通常以下划线变量的形式传递参数,如:user_info。后端通常使用驼峰命名参数变量,如:userInfo。如过入参是单个参数,使用 @requestParam("user_info") 来映射下划线变量,但如果以对象方接收参数时这种方式就不适合了,此时就需要做入参绑定。
入参解析实际上需要实现 HandlerMethodArgumentResolver 接口,可以自定义注解来做,实现 HandlerMethodArgumentResolver 接口中的 resolveArgument 方法,使用的时候就得加上注解才能解析,这里我们将继承 ModelAttributeMethodProcessor 类来实现参数解析,使用时无需加注解。
关键类如下:
自定义 ServletRequestDataBinder:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package com.mayee.config;import org.springframework.beans.MutablePropertyValues;import org.springframework.beans.PropertyValue;import org.springframework.web.bind.ServletRequestDataBinder;import javax.servlet.ServletRequest;import java.util.LinkedList;import java.util.List;import java.util.regex.Matcher;import java.util.regex.Pattern;public class CustomServletRequestDataBinder extends ServletRequestDataBinder { public CustomServletRequestDataBinder (Object target) { super (target); } private final static Pattern UNDERLINE_PATTERN = Pattern.compile("_(\\w)" ); @Override protected void addBindValues (MutablePropertyValues mpvs, ServletRequest request) { List<PropertyValue> pvs = mpvs.getPropertyValueList(); List<PropertyValue> adds = new LinkedList <>(); for (PropertyValue pv : pvs) { String name = pv.getName(); String camel = this .underLineToCamel(name); if (!name.equals(camel)) { adds.add(new PropertyValue (camel, pv.getValue())); } } pvs.addAll(adds); } private String underLineToCamel (final String value) { final StringBuffer sb = new StringBuffer (); Matcher m = UNDERLINE_PATTERN.matcher(value); while (m.find()) { m.appendReplacement(sb, m.group(1 ).toUpperCase()); } m.appendTail(sb); return sb.toString(); } }
自定义 ModelAttributeMethodProcessor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.mayee.config;import org.springframework.util.Assert;import org.springframework.web.bind.WebDataBinder;import org.springframework.web.context.request.NativeWebRequest;import org.springframework.web.method.annotation.ModelAttributeMethodProcessor;import javax.servlet.ServletRequest;public class CustomServletModelAttributeMethodProcessor extends ModelAttributeMethodProcessor { public CustomServletModelAttributeMethodProcessor (boolean annotationNotRequired) { super (annotationNotRequired); } @Override protected void bindRequestParameters (WebDataBinder binder, NativeWebRequest request) { ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class); Assert.state(servletRequest != null , "No ServletRequest" ); new CustomServletRequestDataBinder (binder.getTarget()).bind(servletRequest); } }
然后将其加入到 webmvc 配置中即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.mayee.config;import org.springframework.context.annotation.Configuration;import org.springframework.web.method.support.HandlerMethodArgumentResolver;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import java.util.List;@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor ()); } @Override public void addArgumentResolvers (List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(new CustomServletModelAttributeMethodProcessor (true )); } }
注意:CustomServletModelAttributeMethodProcessor 这个类我继承的是 ModelAttributeMethodProcessor 而不是 ServletModelAttributeMethodProcessor。
因为在 ModelAttributeMethodProcessor 中的 resolveArgument 方法中,调用了 validateIfApplicable 方法来做 JSR-303 参数校验。但 ServletModelAttributeMethodProcessor 中重写了 resolveConstructorArgument 方法,没有了参数校验。
Tip:本文完整示例代码已上传至 Gitee