SpringBoot 集成 openfeign 及入参绑定
前言
微服务开发时必定涉及到服务注册、服务发现及服务间的通信,一般而言,一个大型系统被拆分为多个小服务,则服务之间使用 dubbo 以 RPC 方式通信,会更快、更轻量、并发高,而不同系统之间的远程调用可以使用 feign 的以 HTTP 的方式更适合,而无论是用哪种方式都绕不开服务注册与发现。本文中我们将使用 openfeign 来作为通信方式,使用 Nacos 完成服务注册与发现,另外将介绍 Spring 入参绑定。
1. SpringBoot 集成 openFeign
1.1. 启动 nacos
使用 docker-compose 方式部署单机 nacos,docker-compose.yml
文件如下:
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。
1.2. 创建 SpringBoot 工程
创建两个Springboot工程 feign-service-a
、feign-service-b
。
注意:pom.xml 文件中需引入以下依赖,以支持服务名调用,实现负载均衡。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>
使用示例:
- GET 请求单个参数
@GetMapping("/hello")
R hello(@RequestParam String name);
注意:@RequestParam
注解必须添加,否则 name
参数会被放到 body 中,feign 自动将 GET 请求转化为 POST 请求造成错误。
- GET 请求对象参数
@GetMapping("/query")
R query(@SpringQueryMap User user);
注意:参数为对象时必须添加 @SpringQueryMap
注解才能正确解析参数。
- POST 请求体参数
@PostMapping("/create")
R create(@RequestBody User user);
- Path 参数
@DeleteMapping("/{id}")
R delete(@PathVariable Integer id);
- 动态 url
@RequestMapping("/a")
@FeignClient(name = "dynamicURLFeign", url = "http://127.0.0.1:8888")
public interface DynamicURLFeign {
/**
* @param uri
* @Description: 在参数中指定 url,请求时将访问参数 uri 的地址
* @return: java.lang.String
* @Author: Bobby.Ma
* @Date: 2022/2/16 0:48
*/
@GetMapping("/dynamic")
R dynamic(URI uri);
}
@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 链接,不方便进行路由判断;
因此,我发现了下面这种的方式:
@Override
public void apply(RequestTemplate template) {
template.header("auth", "feign-service-b");
Target<?> target = template.feignTarget();
if ("/a/dynamic1".equals(template.path())) {
// 要实现动态 url,这一步是必须的
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 参数哦。
2. Spring 入参绑定
前后端分离开发中,前端通常以下划线变量的形式传递参数,如:user_info
。后端通常使用驼峰命名参数变量,如:userInfo
。如过入参是单个参数,使用 @requestParam("user_info")
来映射下划线变量,但如果以对象方接收参数时这种方式就不适合了,此时就需要做入参绑定。
入参解析实际上需要实现 HandlerMethodArgumentResolver
接口,可以自定义注解来做,实现 HandlerMethodArgumentResolver
接口中的 resolveArgument
方法,使用的时候就得加上注解才能解析,这里我们将继承 ModelAttributeMethodProcessor
类来实现参数解析,使用时无需加注解。
关键类如下:
自定义 ServletRequestDataBinder
:
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)");
/**
* 遍历请求参数对象 把请求参数的名转换成驼峰体
* 重写addBindValues绑定数值的方法
*
* @param mpvs 请求参数列表
* @param request 请求
*/
@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);
}
/**
* 下划线转驼峰
*
* @param value 要转换的下划线字符串
* @return 驼峰体字符串
*/
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
:
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 {
/**
* Class constructor.
*
* @param annotationNotRequired if "true", non-simple method arguments and
* return values are considered model attributes with or without a
* {@code @ModelAttribute} annotation
*/
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 配置中即可:
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) {
// GET 请求,入参对象属性下划线绑定。若入参不是封装的对象,则需要用 @RequestParam 来绑定
resolvers.add(new CustomServletModelAttributeMethodProcessor(true));
}
}
注意:CustomServletModelAttributeMethodProcessor
这个类我继承的是 ModelAttributeMethodProcessor
而不是 ServletModelAttributeMethodProcessor
。
因为在 ModelAttributeMethodProcessor
中的 resolveArgument
方法中,调用了 validateIfApplicable
方法来做 JSR-303
参数校验。但 ServletModelAttributeMethodProcessor
中重写了 resolveConstructorArgument
方法,没有了参数校验。
Tip:本文完整示例代码已上传至 Gitee