跳至主要內容

SpringBoot 集成 openfeign 及入参绑定

Mayee...大约 5 分钟

前言

微服务开发时必定涉及到服务注册、服务发现及服务间的通信,一般而言,一个大型系统被拆分为多个小服务,则服务之间使用 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-afeign-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 参数即可向指定的地址发起请求。
我们可以看到这种方式其实有几个缺点:

    1. feign 调用的方法中需要传与方法无关的参数 URI,并且每个方法中都需要写,很麻烦;
    1. URI 会抛出受查异常,使得必须去处理这种异常,不够美观;
    1. 拦截器中 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:本文完整示例代码已上传至 Giteeopen in new window