前言

微服务开发时必定涉及到服务注册、服务发现及服务间的通信,一般而言,一个大型系统被拆分为多个小服务,则服务之间使用 dubbo 以 RPC 方式通信,会更快、更轻量、并发高,而不同系统之间的远程调用可以使用 feign 的以 HTTP 的方式更适合,而无论是用哪种方式都绕不开服务注册与发现。本文中我们将使用 openfeign 来作为通信方式,使用 Nacos 完成服务注册与发现,另外将介绍 Spring 入参绑定。

一、SrpingBoot 集成 openfeign

  1. 启动 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。

  1. 创建 Springboot 工程

创建两个Springboot工程 feign-service-afeign-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>

使用示例:

  • GET 请求单个参数
1
2
@GetMapping("/hello")
R hello(@RequestParam String name);

注意:@RequestParam 注解必须添加,否则 name 参数会被放到 body 中,feign 自动将 GET 请求转化为 POST 请求造成错误。

  • GET 请求对象参数
1
2
@GetMapping("/query")
R query(@SpringQueryMap User user);

注意:参数为对象时必须添加 @SpringQueryMap 注解才能正确解析参数。

  • POST 请求体参数
1
2
@PostMapping("/create")
R create(@RequestBody User user);
  • Path 参数
1
2
@DeleteMapping("/{id}")
R delete(@PathVariable Integer id);
  • 动态 url
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 {

/**
* @param uri
* @Description: 在参数中指定 url,请求时将访问参数 uri 的地址
* @return: java.lang.String
* @Author: Bobby.Ma
* @Date: 2022/2/16 0:48
*/
@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 参数即可向指定的地址发起请求。

我们可以看到这种方式其实有几个缺点:

    1. feign 调用的方法中需要传与方法无关的参数 URI,并且每个方法中都需要写,很麻烦;
    1. URI 会抛出受查异常,使得必须去处理这种异常,不够美观;
    1. 拦截器中 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())) {
// 要实现动态 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 参数哦。

二、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)");

/**
* 遍历请求参数对象 把请求参数的名转换成驼峰体
* 重写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

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 {
/**
* 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 配置中即可:

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) {
// GET 请求,入参对象属性下划线绑定。若入参不是封装的对象,则需要用 @RequestParam 来绑定
resolvers.add(new CustomServletModelAttributeMethodProcessor(true));
}
}

注意:CustomServletModelAttributeMethodProcessor 这个类我继承的是 ModelAttributeMethodProcessor 而不是 ServletModelAttributeMethodProcessor

因为在 ModelAttributeMethodProcessor 中的 resolveArgument 方法中,调用了 validateIfApplicable 方法来做 JSR-303 参数校验。但 ServletModelAttributeMethodProcessor 中重写了 resolveConstructorArgument 方法,没有了参数校验。


Tip:本文完整示例代码已上传至 Gitee