SpringCloud实践系列(三):OpenFeign服务调用
Nacos: 注册中心,解决服务注册与发现
Ribbon: 客户端的负载均衡器,解决服务集群的负载均衡
SpringCloud实践系列(三):OpenFeign服务调用
OpenFeign:声明式的HTTP客户端,服务远程调用
SpringCloud实践系列(四):Nacos配置中心(待更新)
Nacos:配置中心,中心化管理配置文件
SpringCloud实践系列(五):Sentinel流控(待更新)
Sentinel:微服务流量卫兵,以流量为入口,保护微服务,防止出现服务雪崩
SpringCloud实践系列(六):Gateway网关(待更新)
Gateway: 微服务网关,服务集群的入口,路由转发以及负载均衡(结合Sentinel)
SpringCloud实践系列(七):Sleuth链路追踪(待更新)
Sleuth: 链路追踪,链路快速梳理、故障定位等
SpringCloud实践系列(八):Seata分布式事务(待更新)
Seata: 分布式事务解决方案
在开发 Spring Cloud 微服务的时候,我们知道,服务之间都是以 HTTP 接口的形式对外提供服务的,因此消费者在进行调用的时候,底层就是通过 HTTP Client 的这种方式进行访问。当然我们可以使用JDK原生的 URLConnection、Apache 的 HTTP Client、Netty 异步 Http Client,Spring 的 RestTemplate 去实现服务间的调用。但是最方便、最优雅的方式是通过 Spring Cloud Open Feign 进行服务间的调用 Spring Cloud 对 Feign 进行了增强,使 Feign 支持 Spring Mvc 的注解,并整合了 Ribbon 等,从而让 Feign 的使用更加方便。
接下来就简单讲述一下Feign的入门使用
一、引入依赖及配置编写
- 引入依赖
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-gson</artifactId>
<version>10.2.3</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
- 配置文件
可以不配,有默认值
feign:
client:
config:
cloud-goods: # 指定项目
connect-timeout: 1000 # 设置cloud-goods服务的创建连接超时时间为1000ms
read-timeout: 10 # 设置cloud-goods服务的响应超时时间为10ms
default: # 默认项目
connect-timeout: 1000
read-timeout: 10
- 编写配置类
📢:一旦编写了配置类,编写接口时,spring的注解就不起作用了(不能使用@GetMapping等注解了),要使用它自己的注解 如 RequestLine。
@Configuration
public class FeignConfiguration {
@Bean
public Contract feignContract() {
return new Contract.Default();
}
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
Decoder feignDecoder() {
return new GsonDecoder();
}
@Bean
Encoder feignEncoder() {
return new GsonEncoder();
}
}
- 在启动类上加上注解
// 开启 Feign 扫描支持
@EnableFeignClients
二、编写Feign接口及使用
1、编写Feign接口
方式一:第一步编写了配置类的话,只能使用这种,因为编写了配置文件,spring的注解就不起作用了(不能使用@GetMapping等注解了),要使用它自己的注解。不然会报错:not annotated with HTTP method type (ex. GET, POST)
@FeignClient(name = "myApi", url = "http://localhost:8080")
public interface MyService {
// 调用另外一个服务的接口(get传参)
@RequestLine("GET /user/getUsers?searchString={searchString}")
@Headers("Content-Type: application/json")
List<User> getUsers(@Param("searchString") String searchString);
// Post body传参
@RequestLine("POST /user/updateUser")
@Headers("Content-Type: application/json")
@Body("user")
Object updateUser(User user);
}
方式二:在没有编写配置类的情况下使用这种
@FeignClient(name = "myApi", url = "http://localhost:8080")
@RequestMapping("/user")
public interface MyService {
// 调用另外一个服务的接口(get传参)
@GetMapping("/getUsers")
List<User> getUsers(@RequestParam("searchString") String searchString);
// Post body传参
@PostMapping("/updateUser")
Object updateUser(User user);
}
注意📢:
// 当与nacos结合使用时,这样写是不对的
@FeignClient(name = "myApi", url = "http://cloud-goods")
// 需要这样写
@FeignClient(name = "cloud-goods")
2、使用接口
Feign接口不需要实现类,可直接调用
private MyService myService;
@GetMapping("/userList")
public List<User> getUsers(@RequestParam String searchString){
List<User> userList = myService.getUsers(searchString);
return userList;
}
三、携带token请求
为了安全考虑要访问的服务的接口需要token验证才能访问,因此需要携带token才能访问。
关于新的服务搭建安全框架,使用与要访问的平台一致的token生成和验证机制,这里就不赘述了。
1、方案一:直接在@Headers注解中加token
这种方案可以用来测试,因为,这种方式token是写死的,不能根据浏览器携带的token进行验证。
@FeignClient(name = "myApi", url = "http://localhost:8080")
public interface MyService {
@RequestLine("GET /getUsers?searchString={searchString}")
// 直接在@Headers注解中加token
@Headers({"Content-Type: application/json", "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdW..."})
List<User> getUsers(@Param("searchString") String searchString);
}
2、方案二:根据浏览器动态获取token
- 如何从浏览器中拿到token
可以看到javax.servlet.http包下有个getHeader的方法,可以获得当前浏览器Header中的信息。
-
如何将token放到跨域请求中
在fegin包中的请求拦截器RequestInterceptor有个apply方法,该方法的默认实现如下:
可以看到,默认的Authorization是通过用户名和密码进行base64加密得到的,跟我们的token生成方式不一样,所以直接使用默认的是无法验证通过的,因此,只需实现RequestInterceptor,重写apply方法即可
-
合并 编写配置类,实现RequestInterceptor,重写apply方法,把浏览器header拿到的token放进去。
@Slf4j
@Configuration
@AllArgsConstructor
public class NimBusRequestInterceptor implements RequestInterceptor {
private HttpServletRequest req;
private static final String HEADER_STRING = "Authorization";
@Override
public void apply(RequestTemplate requestTemplate) {
// 如果header没有auth头,从cookie获取token
String token = req.getHeader(HEADER_STRING);
Cookie[] cookies = req.getCookies();
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
if (Objects.equals(cookie.getName(), "token")) {
try {
token = URLDecoder.decode(cookie.getValue(), StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
log.error(LogUtil.getStack(e));
}
}
}
}
requestTemplate.header(HEADER_STRING, token);
}
}
OK,以上就实现了Feign基本使用与携带token请求
四、用户名密码访问及绕过ssl验证
当我们访问一个url时需要用户名密码、ssl证书验证,那就不是简单的加个@FeignClient注解,填个url了,需要我们编写配置文件实现
一、配置文件
@Configuration
public class FeignConfiguration {
@Bean
public Contract feignContract() {
return new Contract.Default();
}
// 配置密码和用户名
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("用户名", "密码");
}
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
Decoder feignDecoder() {
return new GsonDecoder();
}
@Bean
Encoder feignEncoder() {
return new GsonEncoder();
}
// 配置绕过ssl
@Bean
public Client client() throws NoSuchAlgorithmException,
KeyManagementException {
return new Client.Default(
new NaiveSSLSocketFactory("ip"),
new NaiveHostnameVerifier("ip"));
}
}
二、实现配置文件中的两个类
- NaiveSSLSocketFactory
package com.enmotech.emcs.search.search;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class NaiveSSLSocketFactory extends SSLSocketFactory {
private final SSLSocketFactory sslSocketFactory =
(SSLSocketFactory) SSLSocketFactory.getDefault();
private final SSLContext alwaysAllowSslContext;
private final Set<String> naivelyTrustedHostnames;
public NaiveSSLSocketFactory(String ... naivelyTrustedHostnames)
throws NoSuchAlgorithmException, KeyManagementException {
this.naivelyTrustedHostnames =
Collections.unmodifiableSet(
new HashSet<>(Arrays.asList(naivelyTrustedHostnames)));
alwaysAllowSslContext = SSLContext.getInstance("TLS");
TrustManager tm = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
alwaysAllowSslContext.init(null, new TrustManager[] { tm }, null);
}
@Override
public String[] getDefaultCipherSuites() {
return sslSocketFactory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return sslSocketFactory.getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
return (naivelyTrustedHostnames.contains(host))
? alwaysAllowSslContext.getSocketFactory().createSocket(socket, host, port, autoClose)
: sslSocketFactory.createSocket(socket, host, port, autoClose);
}
@Override
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
return (naivelyTrustedHostnames.contains(host))
? alwaysAllowSslContext.getSocketFactory().createSocket(host, port)
: sslSocketFactory.createSocket(host, port);
}
@Override
public Socket createSocket(String host, int port, InetAddress localAddress, int localPort) throws IOException, UnknownHostException {
return (naivelyTrustedHostnames.contains(host))
? alwaysAllowSslContext.getSocketFactory().createSocket(host, port, localAddress, localPort)
: sslSocketFactory.createSocket(host, port, localAddress, localPort);
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return (naivelyTrustedHostnames.contains(host.getHostName()))
? alwaysAllowSslContext.getSocketFactory().createSocket(host, port)
: sslSocketFactory.createSocket(host, port);
}
@Override
public Socket createSocket(InetAddress host, int port, InetAddress localHost, int localPort) throws IOException {
return (naivelyTrustedHostnames.contains(host.getHostName()))
? alwaysAllowSslContext.getSocketFactory().createSocket(host, port, localHost, localPort)
: sslSocketFactory.createSocket(host, port, localHost, localPort);
}
}
- NaiveHostnameVerifier
package com.enmotech.emcs.search.search;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class NaiveHostnameVerifier implements HostnameVerifier {
private final Set<String> naivelyTrustedHostnames;
private final HostnameVerifier hostnameVerifier =
HttpsURLConnection.getDefaultHostnameVerifier();
public NaiveHostnameVerifier(String ... naivelyTrustedHostnames) {
this.naivelyTrustedHostnames =
Collections.unmodifiableSet(
new HashSet<>(Arrays.asList(naivelyTrustedHostnames)));
}
@Override
public boolean verify(String hostname, SSLSession session) {
return naivelyTrustedHostnames.contains(hostname) ||
hostnameVerifier.verify(hostname, session);
}
}
五、整合Sentinel
【Sentinel的使用,参考:】《SpringCloud实践:Sentinel流控组件》
5.1、为何整合Sentinel
比如我们在购物的时候,查看商品详情页面的时候,里面包含库存信息,商品详情信息,评论信息,这个需求包含的微服务如下:
假设现在评论服务宕机了,那就意味用户发出查看商品请求也无法正常显示了,商品都看不到了,那用户也无法进行下单的操作了。
但是对于用户来说,评论看不到并不影响他购物,所以这时候我们应该对评论服务进行降级处理,返回一个兜底数据(空数据),这样用户的查看商品请求能正常显示,只是评论数据看不到而已,这样的话,用户的下单请求也不会受到影响.
5.2、如何整合
-
引入依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> <version>2.2.1.RELEASE</version> </dependency>
-
开启sentinel
# 激活Sentinel对feign的支持 feign: sentinel: enabled: true
-
启动类
// 主启动类添加该注解 @EnableFeignClients
-
使用
编写使用feignClient接口的实现类,并使用fallback参数指定
-
feignClient接口类
sentinel 整合openFeign无法区分降级与异常处理逻辑,都公用一个fallback降级处理方式。
// 指定fallback参数传入自定义的捕获异常的类 @FeignClient(name="Nacos-Server-Name", fallback = MyFeiginClientFallBack.class) public interface ProductFeignClient { @GetMapping(value="/payment/get/{id}") CommonResult getPayment(@PathVariable("id") Long id); @GetMapping("/payment/get/timeout") CommonResult getPaymentTimeout(); }
-
自定义捕获异常的类
编写使用feignClient接口的实现类
@Component public class MyFeignClientFallBack implements ProductFeignClient { @Override public CommonResult getPayment(Long id) { return new CommonResult(500, "OpenFeign--容错---处理",null); } @Override public CommonResult getPaymentTimeout() { return null; } }
-
转载自:https://juejin.cn/post/7242088199810695223