---
url: /article/no5sja3l/index.md
---
## 前言

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

## 1. SpringBoot 集成 openFeign

### 1.1. 启动 nacos

使用 docker-compose 方式部署单机 nacos，`docker-compose.yml` 文件如下：

```yaml
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 文件中需引入以下依赖，以支持服务名调用，实现负载均衡。

```xml
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    <version>2.2.9.RELEASE</version>
</dependency>
```

使用示例：

* GET 请求单个参数

```java
@GetMapping("/hello")
R hello(@RequestParam String name);
```

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

* GET 请求对象参数

```java
@GetMapping("/query")
R query(@SpringQueryMap User user);
```

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

* POST 请求体参数

```java
@PostMapping("/create")
R create(@RequestBody User user);
```

* Path 参数

```java
@DeleteMapping("/{id}")
R delete(@PathVariable Integer id);
```

* 动态 url

```java
@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);
}
```

```java
@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，并且每个方法中都需要写，很麻烦;
* 2. URI 会抛出受查异常，使得必须去处理这种异常，不够美观;
* 3. 拦截器中 template.path 或 template.url 取到的就不是 mapping 路由，而是整个 url 链接，不方便进行路由判断;

因此，我发现了下面这种的方式：

```java
@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`：

```java
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`：

```java
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 配置中即可：

```java
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：本文完整示例代码已上传至 [GitHub](https://github.com/Blogeee/blog-java/tree/main/_20220108)
