Spring Boot中单独使用OpenFeign代替HttpClient/RestTemplate
背景
在Spring Boot项目中,有时候需要访问其他服务,用到的客户端技术一般是使用HttpClient或者RestTemplate封装为一个小工具类,这种方法可行,但是有些缺点:
- 请求的URL很分散,不一样维护
- 响应信息需要重新反序列化
所以,有没有更好的解决方案,让我们就像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);
}
说明:
- @FeignClient(name = “userService”, url = “${host}”),name可以取名当前接口名称,url表示请求服务的ip+port。
- 完整的请求路径为:url+方法名的path,例如第一个方法请求路径完整为:${host}/getUserToken
- 如果是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>
开启降级
- YML配置
feign:
circuitbreaker:
enabled: true
- 调用配置
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。