SpringCloud快速通关(中)
一.OpenFeign
OpenFeign,是一种 Declarative REST Client,即声明式 Rest 客户端,与之对应的是编程式 Rest 客户端,比如 RestTemplate。
OpenFeign 由注解驱动:
- 指定远程地址:
@FeignClien - 指定请求方式:
@GetMapping、@PostMapping、@DeleteMapping... - 指定携带数据:
@RequestHeader、@RequestParam、@RequestBody... - 指定返回结果:响应模式
其中的 @GetMapping 等注解可以沿用 Spring MVC:
- 当它们标记在 Controller 上时,用于接收请求
- 当他们标记在 FeignClien 上时,用于发送请求
1.1 远程调用
声明式实现

(1)使用时引入以下依赖:(之前已经引入过)
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
(2)在需要调用别的微服务的主启动类上使用以下注解:
开启Feign远程调用功能用: @EnableFeignClients
开启Feign远程调用功能用: @EnableFeignClients
开启Feign远程调用功能用: @EnableFeignClients
@EnableFeignClients
.....
public class OrderMainApplication {public static void main(String[] args) {SpringApplication.run(OrderMainApplication.class, args);}
(3)编写远程调用的客户端

标识Feign客户端用:@FeignClient!!!
标识Feign客户端用:@FeignClient!!!
标识Feign客户端用:@FeignClient!!!
package com.heima.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;@FeignClient(value = "service-product")//标识Feign客户端
public interface ProductFeignClient {//mvc注解的两套使用逻辑//1.标注在controller上的,是接受这样的请求//2.标注在FeignClient上的,是发送这个服务@GetMapping("/product/{id}")Product getProductById(@PathVariable("id") Long id);
}
现在我们修改OrderServiceImpl
@Autowiredprivate ProductFeignClient productFeignClient;@Overridepublic Order createOrder(Long userId, Long productId) {//现在改用注入FeignClient//将之前注释掉Product product = getProductFromRemoteWithLoadBalancerAnno(productId);Product product = productFeignClient.getProductById(productId);//以下代码不变}
输出结果:

第三方API
当我们想要向第三方API远程调用(比如使用阿里云为我们提供的天气的API),但是此时变不需要注册中心了,我们也可以使用OpenFeign
//http://aLiv18.data.moji.com/whapi/json/aLicityweather/condition
@FeignClient(value = "weather-client", url = "http://aLiv18.data.moji.com")//因为不需要使用到注册中心,所以value随便写,但是url要精确指定
public interface WeatherFeignClient {@PostMapping("/whapi/json/aLicityweather/condition")String getWeather(@RequestHeader("Authorization") String auth,@RequestParam("token") String token,@RequestParam("cityId") String cityId);
}

技巧:
如何编写好 OpenFeign 声明式的远程调用接口:
- 针对业务 API:直接复制对方的 Controller 签名即可;
- 第三方 API:根据接口文档确定请求如何发

vs

面试题:
客户端负载均衡与服务端负载均衡的区别:

1.2 进阶配置
日志
在配置文件中指定 feign 接口所在包的日志级别:
logging:level:# 指定 feign 接口所在的包的日志级别为 debug 级别com.heima.feign: debug
向 Spring配置类容器中注册 feign.Logger.Level 对象:
@Bean
public Logger.Level feignlogLevel() {// 指定 OpenFeign 发请求时,日志级别为 FULLreturn Logger.Level.FULL;
}

超时控制

连接超时(connectTimeout),控制第一步建立连接的时间,不配置的话默认 10 秒。
读取超时(readTimeout),默认 60 秒。
如果需要修改默认超时时间,新建application-feign配置文件中进行如下配置:

spring:cloud:openfeign:client:config:# 默认配置default:logger-level: fullconnect-timeout: 1000read-timeout: 2000# 具体 feign 客户端的超时配置,即@FeignClient中的value值service-product:logger-level: full# 连接超时,3000 毫秒connect-timeout: 3000# 读取超时,5000 毫秒read-timeout: 5000
重试机制
远程调用超时失败后,还可以进行多次尝试,如果某次成功则返回 ok,如果多次尝试后依然失败则结束调用,返回错误。
OpenFeign 底层默认使用 NEVER_RETRY,即从不重试策略。
向 Spring 容器中Config类里面添加 Retryer 类型的 Bean:
@Bean
public Retryer retryer() {return new Retryer.Default();
}
拦截器

以请求拦截器为例,自定义的请求拦截器需要实现 RequestInterceptor 接口,并重写 apply() 方法:
(1)新键interceptor包,编写类
package com.heima.interceptor;import feign.RequestInterceptor;
import feign.RequestTemplate;import java.util.UUID;public class XTokenRequestInterceptor implements RequestInterceptor {/*** 请求拦截器** @param template 封装本次请求的详细信息*/@Overridepublic void apply(RequestTemplate template) {System.out.println("XTokenRequestInterceptor ...");template.header("X-Token", UUID.randomUUID().toString());}
}
要想要该拦截器生效有两种方法:
-
在application-feign.yml配置文件中配置对应 Feign 客户端的请求拦截器,此时该拦截器只对指定的 Feign 客户端生效
spring:cloud:openfeign:client:config:# 具体 feign 客户端service-product:# 该请求拦截器仅对当前客户端有效request-interceptors:- com.heima.interceptor.XTokenRequestInterceptor
2. 还可以直接将自定义的请求拦截器添加到 Spring 容器中,此时该拦截器对服务内的所有 Feign客户端生效
@Component
public class XTokenRequestInterceptor implements RequestInterceptor {// --snip--
}
Fallback兜底返回
Fallback,即兜底返回。注意,此功能需要整合 Sentinel 才能实现。
(1) 在order-service中需要先导入 Sentinel 依赖:
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
(2) 并在需要进行 Fallback 的服务的application-feign.yml配置文件中开启配置:
feign:sentinel:enabled: true
(3) 在feign包下建立新的fallback包
package com.heima.feign.fallback;@Component
//实现ProductFeignClient接口,调用远程客户端的时候找不到远程服务,就会调用这个类(假数据)
public class ProductFeignClientFallback implements ProductFeignClient {@Overridepublic Product getProductById(Long id) {System.out.println("兜底回调...");Product product = new Product();product.setId(id);product.setPrice(new BigDecimal("0"));product.setProductName("未知商品");product.setNum(0);return product;}
}
(4)之后回到对应的 Feign 客户端,配置 Fallback:
@FeignClient(value = "service-product", fallback = ProductFeignClientFallback.class)
public interface ProductFeignClient {@GetMapping("/product/{id}")Product getProductById(@PathVariable("id") Long id);
}
二.Sentinel
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Spring Cloud Alibaba Sentinel 以流量为切入点,从流量控制、流量路由、熔断降级、系统自适应过载保护、热点流量防护等多个维度保护服务的稳定性。

定义资源:
-
主流框架自动适配(Web Servlet、Dubbo、Spring Cloud、gRPC、Spring WebFlux、Reactor),所有 Web 接口均为资源
-
编程式:SphU API
-
声明式:
@SentinelResource
定义规则:
-
流量控制(FlowRule)
-
熔断降级(DegradeRule)
-
系统保护(SystemRule)
-
来源访问控制(AuthorityRule)
-
热点参数(ParamFlowRule)

2.1 整合Sentinel
启动 Dashboard
前往 Sentinel GitHub Realease 页下载 Sentinel Dashboard,这里选择 1.8.8 版本,因此下载 sentinel-dashboard-1.8.8.jar。
在 sentinel-dashboard-1.8.8.jar 所在的目录运行以下命令,启动 Dashboard:
java -jar sentinel-dashboard-1.8.8.jar
启动完成后,浏览器访问 http://localhost:8080/,默认用户与密码均为 sentinel。
注意:关闭sentinel我们通过在终端ctrl+c进行关闭
服务整合 Sentinel
java -jar sentinel-dashboard-1.8.8.jar
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
(2) 配置文件中添加:
spring:application:name: service-productcloud:sentinel:transport:# 控制台地址dashboard: localhost:8080# 立即加载服务 eager: true
(3) 配置完成后启动对应服务,再前往 Sentinel Dashboard 查看,能够看到对应服务信息。

(4) 可以在一个方法上使用 @SentinelResource 注解,将其标记为一个「资源」,当方法被调用时,能够在 Dashboard 的「簇点链路」上找到对应的资源,之后在界面上完成对资源的流控、熔断、热点、授权等操作。
eg:假如我认为创建订单createOrder是一个资源,我将来想要保护它,对它进行一些流控制等规则限制
.............@SentinelResource(value = "createOrder")@Overridepublic Order createOrder(Long userId, Long productId) {}
此时
2.2异常处理

Web接口
(1)在model模块下新建一个包common,创建类R
package com.common;import lombok.Data;@Data
public class R {private Integer code;private String msg;private Object data;public static R ok() {R r = new R();r.setCode(200);return r;}public static R ok(Object data){R r = new R();r.setCode(200);r.setData(data);return r;}public static R error(){R r = new R();r.setCode(500);return r;}public static R error(Integer code,String msg){R r = new R();r.setCode(code);r.setMsg(msg);return r;}
}
(2)在service-order模块下新建包exception,创建自定义的类MyBlockExceptionHandler ,可以实现 BlockExceptionHandler 接口,并将实现类交给 Spring 管理
package com.heima.exception;import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.common.R;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;import java.io.PrintWriter;@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {private final ObjectMapper objectMapper;public MyBlockExceptionHandler(ObjectMapper objectMapper) {this.objectMapper = objectMapper;}@Overridepublic void handle(HttpServletRequest request,HttpServletResponse response,String resourceName,BlockException e) throws Exception {response.setContentType("application/json;charset=utf-8");PrintWriter writer = response.getWriter();R error = R.error(500, resourceName + " 被 Sentinel 限制了, 原因: " + e.getClass());String json = objectMapper.writeValueAsString(error);writer.write(json);writer.flush();writer.close();}
}
此时多次访问
@SentinelResource
当 @SentinelResource 注解标记的资源被流控时,默认返回 500 错误页。
如果需要自定义异常处理,一般可以增加
@SentinelResource注解的以下任意配置:
blockHandler
fallback
defaultFallback
以 blockHandler 为例:
@SentinelResource(value = "createOrder", blockHandler = "createOrderFallback")
public Order createOrder(Long productId, Long userId) {// --snip--
}
在当前类中创建名称为 blockHandler 值的方法,并且返回值类型、参数信息与 @SentinelResource 标记的方法一致(可以额外增加一个 BlockException 类型的参数):
/**
* 指定兜底回调
*/
public Order createOrderFallback(Long productId, Long userId, BlockException e) {Order order = new Order();order.setId(0L);order.setTotalAmount(new BigDecimal("0"));order.setUserId(userId);order.setNickname("未知用户");order.setAddress("异常信息: " + e.getClass());return order;
}
此时多次访问该界面,会得到:

Feign 接口
当 Feign 接口作为资源并被流控时,如果调用的 Feign 接口指定了 fallback,那么就会使用 Feign 接口的 fallback 进行异常处理,否则由 SpringBoot 进行全局异常处理。

