Spring Boot中单独使用OpenFeign代替HttpClient/RestTemplate

背景

在Spring Boot项目中,有时候需要访问其他服务,用到的客户端技术一般是使用HttpClient或者RestTemplate封装为一个小工具类,这种方法可行,但是有些缺点:

  1. 请求的URL很分散,不一样维护
  2. 响应信息需要重新反序列化

所以,有没有更好的解决方案,让我们就像controller调用service那样来直接调用其他服务,并且不需要反序列化,直接自动把响应结果封装成为一个对象?
有的!OpenFeign可以在SpringBoot中单独使用!及其方便!
说明:现在使用的是Spring Cloud亲生的OpenFeign,不是奈飞的feign!

单独使用OpenFeign

导包

	<properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>2020.0.5</spring-cloud.version>
    </properties>
    
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

开启openFeign

package com.xywei.springboot.demohttp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients(basePackages = {"com.xywei.springboot.demohttp.openfeign"})
@SpringBootApplication
public class DemoHttpApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoHttpApplication.class, args);
    }

}

@EnableFeignClients(basePackages = {"com.xywei.springboot.demohttp.openfeign"})
其中的**basePackages **表示feign调用所在的包

配置openFeign接口

package com.xywei.springboot.demohttp.openfeign;

import com.xywei.springboot.demohttp.entity.User;
import com.xywei.springboot.demohttp.vo.BaseResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Component
@FeignClient(name = "userService", url = "${host}")
public interface UserServiceHttp {

    @GetMapping("/getUserToken")
    BaseResult getUseToken(@RequestParam("username") String username, @RequestParam("password") String password);

    @GetMapping("/getUserById")
    String getUserById(@RequestParam(value = "user_id") String userId, @RequestHeader(value = "user_token") String userToken);

    @PostMapping("/getUserByUsernamePasswordWithToken")
    BaseResult getUserByUsernamePasswordWithToken(@RequestParam(value = "username") String username,
                                                  @RequestParam(value = "password") String password,
                                                  @RequestHeader("token") String token);

    @PostMapping("/insertUserWithToken")
    BaseResult insertUserWithToken(@RequestBody List<User> users, @RequestHeader("token") String token);
}

说明:

  1. @FeignClient(name = “userService”, url = “${host}”),name可以取名当前接口名称,url表示请求服务的ip+port。
  2. 完整的请求路径为:url+方法名的path,例如第一个方法请求路径完整为:${host}/getUserToken
  3. 如果是Json格式的参数,就使用@RequestBody ,表单或者查询参数格式,就使用@RequestParam,如果是需要请求头,就使用@RequestHeader进行参数绑定。

测试使用OpenFeign

调用方代码

package com.xywei.springboot.demohttp;

import com.xywei.springboot.demohttp.entity.User;
import com.xywei.springboot.demohttp.openfeign.BaiduServiceHttp;
import com.xywei.springboot.demohttp.openfeign.UserServiceHttp;
import com.xywei.springboot.demohttp.vo.BaseResult;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

@SpringBootTest
public class OpenFeignTests {

    @Resource
    private UserServiceHttp userServiceHttp;

    @Resource
    private BaiduServiceHttp baiduServiceHttp;

    @Test
    public void testBaidu() {
        String result = baiduServiceHttp.keyWordSearch("xywei");
        System.out.println(result);
    }

    @Test
    public void testOpenFeignGet() {
        BaseResult userToken = userServiceHttp.getUseToken("hello", "word");
        System.out.println(userToken);
    }

    @Test
    public void testOpenFeignGetWithHeader() {
        String userToken = userServiceHttp.getUserById("hello", "word998");
        System.out.println(userToken);
    }

    @Test
    public void testPostFormData() {
        BaseResult result = userServiceHttp.getUserByUsernamePasswordWithToken("user998", "password998", "token998");
        System.out.println(result);
    }

    @Test
    public void testPostJson() {
        User user = new User("9998", "xywei", "xywei");
        List<User> users = new ArrayList<>();
        users.add(user);
        BaseResult baseResult = userServiceHttp.insertUserWithToken(users, "xywei token 998");
        System.out.println(baseResult);
    }
}

提供方代码

package com.xywei.springboot.springtestdemo.controller;

import com.xywei.springboot.springtestdemo.entity.User;
import com.xywei.springboot.springtestdemo.vo.BaseResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.time.LocalTime;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Slf4j
@RestController
public class UserController {

    @GetMapping("/getUserToken")
    public ResponseEntity<BaseResult> getUseToken(String username, String password) {
        log.info("username: {}, password: {}", username, password);
        BaseResult result = new BaseResult();
        result.setCode(200);
        result.setData(UUID.randomUUID().toString()+ LocalTime.now());
        result.setMessage("success");
        return new ResponseEntity<>(result, HttpStatus.OK);
    }

    /**
     * GET请求,查询参数+请求头<br/>
     * 随机返回用户名和密码
     *
     * @param userId
     * @param userToken
     * @return
     */
    @GetMapping("/getUserById")
    public ResponseEntity<BaseResult> getUserById(@RequestParam(value = "user_id", required = true) String userId,
                                                  @RequestHeader(value = "user_token") String userToken) {

        BaseResult<User> baseResult = new BaseResult<>();
        User user = new User(userId, "whoami-" + userId.hashCode(), UUID.randomUUID().toString() + userToken);
        baseResult.setCode(HttpStatus.OK.value());
        baseResult.setMessage(HttpStatus.OK.getReasonPhrase());
        baseResult.setData(user);

        return new ResponseEntity<BaseResult>(baseResult, HttpStatus.OK);
    }

    /**
     * POST请求,接受表单参数
     * 给什么就返回什么,随机生成id
     *
     * @param username
     * @param password
     * @return
     */
    @PostMapping("/getUserByUsernamePassword")
    public ResponseEntity<BaseResult> getUserByUsernamePassword(String username, String password) {
        BaseResult<User> baseResult = new BaseResult<>();
        User user = new User(UUID.randomUUID().toString(), username, password);
        baseResult.setCode(HttpStatus.OK.value());
        baseResult.setMessage(HttpStatus.OK.getReasonPhrase());
        baseResult.setData(user);
        return new ResponseEntity<BaseResult>(baseResult, HttpStatus.OK);
    }

    /**
     * POST请求,接受表单参数
     * 给什么就返回什么,随机生成id
     *
     * @param username
     * @param password
     * @return
     */
    @PostMapping("/getUserByUsernamePasswordWithToken")
    public ResponseEntity<BaseResult> getUserByUsernamePasswordWithToken(String username, String password, @RequestHeader("token") String token) {
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        BaseResult<User> baseResult = new BaseResult<>();
        User user = new User(UUID.randomUUID().toString(), username, password);
        baseResult.setCode(HttpStatus.OK.value());
        baseResult.setMessage(HttpStatus.OK.getReasonPhrase());
        baseResult.setData(user);
        return new ResponseEntity<BaseResult>(baseResult, HttpStatus.OK);
    }


    /**
     * POST请求,接受JSON数据
     * 给什么就返回什么
     *
     * @param users
     * @return
     */
    @PostMapping("/insertUser")
    public ResponseEntity<BaseResult> insertUser(@RequestBody List<User> users) {
        BaseResult<List<User>> baseResult = new BaseResult<>();
        baseResult.setCode(HttpStatus.OK.value());
        baseResult.setMessage(HttpStatus.OK.getReasonPhrase());
        baseResult.setData(users);
        return new ResponseEntity<BaseResult>(baseResult, HttpStatus.OK);
    }

    /**
     * POST请求,接受JSON数据
     * 给什么就返回什么
     *
     * @param users
     * @return
     */
    @PostMapping("/insertUserWithToken")
    public ResponseEntity<BaseResult> insertUserWithToken(@RequestBody List<User> users, @RequestHeader("token") String token) {
        System.out.println(1/0);
        BaseResult<List<User>> baseResult = new BaseResult<>();
        baseResult.setCode(HttpStatus.OK.value());
        baseResult.setMessage(HttpStatus.OK.getReasonPhrase() + "token:" + token);
        baseResult.setData(users);
        return new ResponseEntity<BaseResult>(baseResult, HttpStatus.OK);
    }

}

说明:服务端返回的信息自动被封装到一个baseResult中,方便我们调用。

特殊配置

配置超时

feign:
  client:
    config:
      default:
        connectTimeout: 1000 # 建立连接的最大等待时间
        readTimeout: 3000 # 获取响应信息的最大等待时间

重试机制

单独使用OpenFeign,如果需要在和服务端通信失败重新通信的话,可以配置重试机制:

package com.xywei.springboot.demohttp.config;

import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 开启重试功能,只对connection timeout & read timeout有效
 *
 */
@Configuration
public class FeignConfig {

    
    @Bean
    public Retryer retryer() {
        //      param1: this.period = period;重试时间间隔,ms
        //      param2: this.maxPeriod = maxPeriod;重试时间最大间隔,ms
        //      param3: this.maxAttempts = maxAttempts;重试的最大次数,包括首次请求
        return new Retryer.Default(100, 1000, 3);
    }
}

说明:假设读取响应信息超时重试,超时最大时间3S,重试次数为3,那么,这个请求需要的时间最大为:超时最大时间*重试最大时间=9S+。

缺点

如果调用失败,抛出的异常需要我们手动捕获:

    @Test
    public void testBaidu() {
        try {
            String result = baiduServiceHttp.keyWordSearch("xywei");
            System.out.println(result);
        } catch (Exception e) {
            // do sth.
        }
    }

解决缺点,不需要手动处理异常

可以再引入新的组件Hystrix来进行服务降级。
可能还有其他更好的处理方法,请各位大佬赐教~

导包

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
            <version>2.2.9.RELEASE</version>
        </dependency>

开启降级

  1. YML配置
feign:
  circuitbreaker:
    enabled: true
  1. 调用配置
package com.xywei.springboot.demohttp.openfeign;

import com.xywei.springboot.demohttp.entity.User;
import com.xywei.springboot.demohttp.openfeign.fallback.UserServiceFallBackFactory;
import com.xywei.springboot.demohttp.vo.BaseResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@FeignClient(name = "userService", url = "${host}", fallbackFactory = UserServiceFallBackFactory.class)
public interface UserServiceHttp {

    @GetMapping("/getUserToken")
    BaseResult getUseToken(@RequestParam("username") String username, @RequestParam("password") String password);

    @GetMapping("/getUserById")
    String getUserById(@RequestParam(value = "user_id") String userId, @RequestHeader(value = "user_token") String userToken);

    @PostMapping("/getUserByUsernamePasswordWithToken")
    BaseResult getUserByUsernamePasswordWithToken(@RequestParam(value = "username") String username,
                                                  @RequestParam(value = "password") String password,
                                                  @RequestHeader("token") String token);

    @PostMapping("/insertUserWithToken")
    BaseResult insertUserWithToken(@RequestBody List<User> users, @RequestHeader("token") String token);
}

编写降级类

package com.xywei.springboot.demohttp.openfeign.fallback;

import com.xywei.springboot.demohttp.entity.User;
import com.xywei.springboot.demohttp.openfeign.UserServiceHttp;
import com.xywei.springboot.demohttp.vo.BaseResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;

import java.util.List;

@Slf4j
@Component
public class UserServiceFallBackFactory implements FallbackFactory<UserServiceHttp> {
    @Override
    public UserServiceHttp create(Throwable cause) {

        log.error("UserServiceHttp error:", cause);

        return new UserServiceHttp() {
            @Override
            public BaseResult getUseToken(String username, String password) {
                return null;// test,
            }

            @Override
            public String getUserById(String userId, String userToken) {
                return null;// test
            }

            @Override
            public BaseResult getUserByUsernamePasswordWithToken(String username, String password, String token) {
                return null;// test
            }

            @Override
            public BaseResult insertUserWithToken(List<User> users, String token) {
                return null;// test
            }
        };
    }
}

说明:如果请求服务发生异常了,则有降级类返回null。

总结

到此单独使用openfeign完毕。但是,还是有些缺点的,例如:虽然调用简便了,但是多引入新的jar包,增加不确定的风险。如果项目不是大量需要请求其他系统,可以直接使用restTemplate。