1.Web接口异常处理
1.1 问题场景
当我们对web接口进行了保护,例如流量控制时,访问量过多时sentinel会直接把错误信息返回:

这是因为sentinel默认是使用一个拦截器来实现的:
java
public abstract class AbstractSentinelInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String resourceName = "";
try {
resourceName = this.getResourceName(request);
if (StringUtil.isEmpty(resourceName)) {
return true;
} else if (this.increaseReference(request, this.baseWebMvcConfig.getRequestRefName(), 1) != 1) {
return true;
} else {
String origin = this.parseOrigin(request);
String contextName = this.getContextName(request);
ContextUtil.enter(contextName, origin);
//资源保护流程
Entry entry = SphU.entry(resourceName, 1, EntryType.IN);
request.setAttribute(this.baseWebMvcConfig.getRequestAttributeName(), entry);
//没有违反规则返回true,违法规则抛出BlockException异常
return true;
}
} catch (BlockException var12) {
BlockException e = var12;
try {
//调用这个方法处理
this.handleBlockException(request, response, resourceName, e);
} finally {
ContextUtil.exit();
}
return false;
}
}
}
handleBlockException()最后会调用下面这个handle进行处理:
java
public class DefaultBlockExceptionHandler implements BlockExceptionHandler {
public DefaultBlockExceptionHandler() {
}
public void handle(HttpServletRequest request, HttpServletResponse response, String resourceName, BlockException ex) throws Exception {
response.setStatus(429);
PrintWriter out = response.getWriter();
out.print("Blocked by Sentinel (flow limiting)");
out.flush();
out.close();
}
}
也就输出了页面里的内容。这样的方式不适合前后端分离项目,我们需要自定义异常处理器统一返回 JSON。
1.2 自定义异常
定义一个统一返回对象:
java
package com.ting.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(String msg, Object data) {
R r = new R();
r.setCode(200);
r.setMsg(msg);
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;
}
}
实现BlockExceptionHandler接口编写自定义返回内容:
java
@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, String s, BlockException e) throws Exception {
PrintWriter writer = httpServletResponse.getWriter();
R error = R.error(500, s + "被Sentinel限制了,原因:" + e.getMessage());
writer.write(objectMapper.writeValueAsString(error));
}
}
再次触发保护时就会返回我们设定好的内容

为什么我们实现了BlockExceptionHandler 就不会走DefaultBlockExceptionHandler 了:
看这段源码:
java
public SentinelWebMvcConfig sentinelWebMvcConfig() {
SentinelWebMvcConfig sentinelWebMvcConfig = new SentinelWebMvcConfig();
sentinelWebMvcConfig.setHttpMethodSpecify(this.properties.getHttpMethodSpecify());
sentinelWebMvcConfig.setWebContextUnify(this.properties.getWebContextUnify());
Optional var10000;
//Optional<BlockExceptionHandler> blockExceptionHandlerOptional;
//isPresent()表示判断Spring容器是否有BlockExceptionHandler的bean
if (this.blockExceptionHandlerOptional.isPresent()) {
//有,直接用
var10000 = this.blockExceptionHandlerOptional;
Objects.requireNonNull(sentinelWebMvcConfig);
var10000.ifPresent(sentinelWebMvcConfig::setBlockExceptionHandler);
} else if (StringUtils.hasText(this.properties.getBlockPage())) {
//如果配置了自定义的限流跳转页面,则使用跳转方式处理异常
sentinelWebMvcConfig.setBlockExceptionHandler((request, response, resourceName, e) -> {
response.sendRedirect(this.properties.getBlockPage());
});
} else {
//使用默认的DefaultBlockExceptionHandler
sentinelWebMvcConfig.setBlockExceptionHandler(new DefaultBlockExceptionHandler());
}
var10000 = this.urlCleanerOptional;
Objects.requireNonNull(sentinelWebMvcConfig);
var10000.ifPresent(sentinelWebMvcConfig::setUrlCleaner);
var10000 = this.requestOriginParserOptional;
Objects.requireNonNull(sentinelWebMvcConfig);
var10000.ifPresent(sentinelWebMvcConfig::setOriginParser);
return sentinelWebMvcConfig;
}
注意:只有会被自动识别的资源(SpringMVC 接口,OpenFeign 远程调用接口,Gateway 网关路由接口)才会使用BlockExceptionHandler处理,@SentinelResource 定义的资源不会走BlockExceptionHandler
2. @SentinelResource添加的资源
2.1 源码解析
这里我们修改一下项目代码;
java
@RestController
public class OrderController {
@Autowired
OrderService orderService;
@GetMapping("/order")
public Order createOrder( @RequestParam("userId") Long userId,
@RequestParam("productId") Long productId) {
return orderService.createOrder(userId, productId);
}
}
java
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
ProductFeignClient productFeignClient;
@SentinelResource(value = "createOrder")
@Override
public Order createOrder(Long userId, Long productId) {
log.info("调用了OrderServiceImpl.createOrder(Long userId, Long productId)");
Product product = productFeignClient.getProductById(productId);
Order order = new Order();
order.setId(productId);
order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum())));
order.setUserId(userId);
order.setNickName("Ting");
order.setAddress("北京");
order.setProductList(Arrays.asList(product));
return order;
}
}
添加Service层,把业务逻辑移动到service层,并且把createOrder方法标注为createOrder资源,调用一次后我们就可以在sentinel控制台看见这个资源:

我们对其进行流量控制,快速点击触发保护:

可以发现并没有走BlockExceptionHandler进行处理,这是因为BlockExceptionHandler是基于web拦截器进行实现的,只能对于SpringMVC 接口,OpenFeign 远程调用接口,Gateway 网关路由接口,这种涉及到请求的资源生效。@SentinelResource 手动资源保护是基于SpringAOP实现的:

在SentinelResurceAspect这个类里我们可以看到到,定义了一个切点即**@SentinelResource** 注解,添加了这个注解的方法就会使用下面的invokeResourceWithSentinel方法 进行增强,可以看到在执行pjp.proceed() 之前调用了SphU.entry() 即检查是否违法了规则,如果正常则继续执行,异常则会抛出BlockException 异常被下面的catch捕获进而调用**handleBlockException()**方法进行处理。

在**handleBlockException()**方法中我们可以看到,检查了@SentinelResource注解中是否设置了blockHandler,如果设置了由invoke()方法处理,没有则由handleFallback()方法处理。在我们刚才的情况中我们没有设置任何东西,所有代码在这里会进入handleFallback()方法:

这里可以看到handleFallback()方法调用了他自己的一个重载方法,其中传入了两个关键参数:
annotation.fallback()和annotation.defaultFallback()
在下面方法中,首先通过fallback参数尝试获取了fallback方法,如果有则通过这个方法处理,但是我们并没有设置,所以这里获取的结果是null,会直接进入else中,调用handleDefaultFallback()方法通过默认的fallback(annotation.defaultFallback())进行处理。

在handleDefaultFallback()方法中先通过DefaultFallback参数尝试获取了默认fallback方法,但是我们什么都没有在@SentinelResource注解中设置,所以获取到的同样是null,这里直接进入了else也就是直接把异常抛出。
2.2 blockHandle
通过这个属性指定一个兜底回调,方法必须和原方法参数、返回值完全一致 可以额外添加BlockException属性:
java
@SentinelResource(value = "createOrder", blockHandler = "createOrderFallback")
@Override
public Order createOrder(Long userId, Long productId) {
log.info("调用了OrderServiceImpl.createOrder(Long userId, Long productId)");
Product product = productFeignClient.getProductById(productId);
Order order = new Order();
order.setId(productId);
order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum())));
order.setUserId(userId);
order.setNickName("Ting");
order.setAddress("北京");
order.setProductList(Arrays.asList(product));
return order;
}
//兜底回调
public Order createOrderFallback(Long userId, Long productId, BlockException e) {
Order order = new Order();
order.setId(0L);
order.setTotalAmount(new BigDecimal("0"));
order.setUserId(0L);
order.setNickName("出现异常:" + e.getClass());
order.setAddress("");
return order;
}
当再次触发限流时就会触发我们的兜底回调:

注意:blockHandler只处理限流 / 熔断(BlockException)导致的异常
2.3 fallback
fallback 只处理业务异常(运行时异常),不会处理BlockException异常,方法必须和原方法参数、返回值完全一致,可以额外加 Throwable 参数。
注意:Sentinel 默认 不会捕获业务异常 ,运行时异常会直接抛出去,不走 fallback,我们需要在配置文件中设置:
java
spring.cloud.sentinel.enabled=true
java
@SentinelResource(value = "createOrder", blockHandler = "createOrderFallback", fallback = "createOrderRuntimeExceptionFallback")
@Override
public Order createOrder(Long userId, Long productId) {
log.info("调用了OrderServiceImpl.createOrder(Long userId, Long productId)");
Order test = null;
test.getAddress();
Product product = productFeignClient.getProductById(productId);
Order order = new Order();
order.setId(productId);
order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum())));
order.setUserId(userId);
order.setNickName("Ting");
order.setAddress("北京");
order.setProductList(Arrays.asList(product));
return order;
}
//兜底回调
public Order createOrderRuntimeExceptionFallback(Long userId, Long productId, Throwable e) {
Order order = new Order();
order.setId(0L);
order.setTotalAmount(new BigDecimal("0"));
order.setUserId(0L);
order.setNickName("出现运行时异常异常:" + e.getClass());
order.setAddress("");
return order;
}
这我模拟了一个空指针的场景:

2.4 defaultFallback
和fallback类似,只处理业务异常(运行时异常),不会处理BlockException异常,通常用于对当前类多个业务方法做兜底返回,返回值必须和业务方法一致,支持无参或Throwable参数,当未指定fallback或者指定了未实现时会使用defaultFallback处理运行时异常:
java
@SentinelResource(value = "createOrder",
blockHandler = "createOrderFallback",
defaultFallback = "OrderRuntimeExceptionDefaultFallback"
)
@Override
public Order createOrder(Long userId, Long productId) {
log.info("调用了OrderServiceImpl.createOrder(Long userId, Long productId)");
Order test = null;
test.getAddress();
Product product = productFeignClient.getProductById(productId);
Order order = new Order();
order.setId(productId);
order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum())));
order.setUserId(userId);
order.setNickName("Ting");
order.setAddress("北京");
order.setProductList(Arrays.asList(product));
return order;
}
public Order OrderRuntimeExceptionDefaultFallback(Throwable e) {
Order order = new Order();
order.setId(0L);
order.setTotalAmount(new BigDecimal("0"));
order.setUserId(0L);
order.setNickName("出现运行时异常异常:" + e.getClass());
order.setAddress("OrderDefaultFallback");
return order;
}
3. Feign远程调用资源
3.1 使用示例
在前面OpenFeign章节,我们已经写过示例:
java
@FeignClient(value = "service-product", fallback = ProductFeignClientFallback.class)
public interface ProductFeignClient {
@GetMapping("/product/{id}")
Product getProductById(@PathVariable("id") Long id);
}
java
@Component
public class ProductFeignClientFallback implements ProductFeignClient {
@Override
public Product getProductById(Long id) {
Product product = new Product();
product.setId(666L);
product.setPrice(new BigDecimal("636"));
product.setProductName("xiaomi666");
product.setNum(777);
return product;
}
}
编写好fallback,在@FeignClient注解中指定fallback方法所在类即可
3.2 源码解析
在SentinelFeignAutoConfiguration,这个Sentinel和OpenFeign整合配置类中,这里可以看到注册了Feign.Builder到spring容器中,这里面就包含了所有的Feign客户端:
在这个类中内部构建方法可以看到,这里获取并判断了fallback是否存在,最后整合进了SentinelInvocationHandler
在这个类的invoke方法中就可以看到我们熟悉的逻辑,先判断是否违法规则,如果违法抛出异常,再判断是否有fallback,没有直接把异常抛出
4.SphU硬编码控制
我们可以通过SphU的entry方法对任意一段代码进行保护,这个方法了解即可
java
public Order createOrder(Long userId, Long productId) {
log.info("调用了OrderServiceImpl.createOrder(Long userId, Long productId)");
// Order test = null;
// test.getAddress();
Product product = productFeignClient.getProductById(productId);
Order order = new Order();
try {
SphU.entry("resourceName");
order.setId(productId);
order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum())));
order.setUserId(userId);
order.setNickName("Ting");
order.setAddress("北京");
order.setProductList(Arrays.asList(product));
} catch (BlockException e) {
//编码处理
}
return order;
}