spring cloud gateway 熔断 304 处理方案
前提:这里只针对304处理,302等其他请使用其他方案,如果有小伙伴有更好的方案请提出讨论
首先我们必须知道今静态资源304状态,是有两个http 头部信息决定的,Last-Modified 和If-Modified-Since
Last-Modified 是由服务器发送给客户端的HTTP请求头标签
If-Modified-Since 则是由客户端发送给服务器的HTTP请求头标签
第一次浏览器请求的时候是没有If-Modified-Since,当请求成功服务器会把该静态资源的Last-Modified(最后一次修改时间)返回
而后浏览器再次请求静态资源的时候会携带该头部信息的If-Modified-Since ,服务器会把If-Modified-Since和Last-Modified 进行比对,如果一致,则返回304状态码告诉浏览器,你本地已经缓存,直接使用即可;
问题就在于返回304后,服务端是没有body内容的,而gateway 在处理熔断的时候是查看后台是否有body进行判断的。且熔断机制在gateway中优先级非常高;从而导致gateway在对304进行处理的时候以为服务器熔断而降级;使得这种类型的静态资源 无法加载;
解决路程:
1.我暂时关闭了熔断,使用全局异常处理方式获得后台返回,如果为空,则设置状态码为304;并设置body 为随意,(浏览器会根据状态码来进行业务处理,而不会根据body内容:(切记:后端服务请进行全局异常处理,非304状态下的异常最好不要返回null,否则会会增加gateway的逻辑复杂度))代码如下:
/** * * @author Administrator * @version $Id: JsonExceptionHandler, v 0.1 2020/3/16 22:48 Administrator Exp$ */ public class JsonExceptionHandler implements ErrorWebExceptionHandler { private static final Logger log = LoggerFactory.getLogger(JsonExceptionHandler.class); /** * MessageReader */ private List<HttpMessageReader<?>> messageReaders = Collections.emptyList(); /** * MessageWriter */ private List<HttpMessageWriter<?>> messageWriters = Collections.emptyList(); /** * ViewResolvers */ private List<ViewResolver> viewResolvers = Collections.emptyList(); /** * 存储处理异常后的信息 */ private ThreadLocal<Map<String, Object>> exceptionHandlerResult = new ThreadLocal<>(); /** * 参考AbstractErrorWebExceptionHandler * * @param messageReaders */ public void setMessageReaders(List<HttpMessageReader<?>> messageReaders) { Assert.notNull(messageReaders, "'messageReaders' must not be null"); this.messageReaders = messageReaders; } /** * 参考AbstractErrorWebExceptionHandler * * @param viewResolvers */ public void setViewResolvers(List<ViewResolver> viewResolvers) { this.viewResolvers = viewResolvers; } /** * 参考AbstractErrorWebExceptionHandler * * @param messageWriters */ public void setMessageWriters(List<HttpMessageWriter<?>> messageWriters) { Assert.notNull(messageWriters, "'messageWriters' must not be null"); this.messageWriters = messageWriters; } @Override public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) { /** * 按照异常类型进行处理 */ ServerHttpRequest request = exchange.getRequest(); HttpStatus httpStatus; String body; if (ex instanceof NotFoundException) { httpStatus = HttpStatus.NOT_FOUND; body = "Service Not Found"; } else if (ex instanceof ResponseStatusException) { ResponseStatusException responseStatusException = (ResponseStatusException) ex; httpStatus = responseStatusException.getStatus(); body = responseStatusException.getMessage(); } else if (ex instanceof NullPointerException) { if (testLast(request.getPath().value())) { body = "Internal Server Error";//这里即使设置body,去前端也会进行304 httpStatus = HttpStatus.NOT_MODIFIED;//状态码设置 } else { body = null;//这里留个口子,其他状态下依旧可以降级 httpStatus = HttpStatus.OK; } } else { httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; body = "Internal Server Error"; } /** * 封装响应体,此body可修改为自己的jsonBody */ Map<String, Object> result = new HashMap<>(2, 1); result.put("httpStatus", httpStatus); result.put("body", body); /** * 错误记录 */ log.error("[全局异常处理]异常请求路径:{},记录异常信息:{}", request.getPath(), ex.getMessage()); /** * 参考AbstractErrorWebExceptionHandler */ if (exchange.getResponse().isCommitted()) { return Mono.error(ex); } exceptionHandlerResult.set(result); ServerRequest newRequest = ServerRequest.create(exchange, this.messageReaders); return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse).route(newRequest) .switchIfEmpty(Mono.error(ex)) .flatMap((handler) -> handler.handle(newRequest)) .flatMap((response) -> write(exchange, response)); } //根据文件后缀进行判断放行 public static boolean testLast(String url) { if (url.lastIndexOf(".js") >= 0 || url.lastIndexOf(".css") >= 0 || url.lastIndexOf(".jpg") >= 0 || url.lastIndexOf(".png") >= 0) { return true; } return false; } /** * 参考DefaultErrorWebExceptionHandler * * @param request * @return */ protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) { Map<String, Object> result = exceptionHandlerResult.get(); return ServerResponse.status((HttpStatus) result.get("httpStatus")) .contentType(MediaType.APPLICATION_JSON_UTF8) .body(BodyInserters.fromObject(result.get("body"))); } /** * 参考AbstractErrorWebExceptionHandler * * @param exchange * @param response * @return */ private Mono<? extends Void> write(ServerWebExchange exchange, ServerResponse response) { exchange.getResponse().getHeaders() .setContentType(response.headers().getContentType()); return response.writeTo(exchange, new ResponseContext()); } /** * 参考AbstractErrorWebExceptionHandler */ private class ResponseContext implements ServerResponse.Context { @Override public List<HttpMessageWriter<?>> messageWriters() { return JsonExceptionHandler.this.messageWriters; } @Override public List<ViewResolver> viewResolvers() { return JsonExceptionHandler.this.viewResolvers; } } }
/** * 在启动类中加入 * 自定义异常处理[@@]注册Bean时依赖的Bean,会从容器中直接获取,所以直接注入即可 * * @param viewResolversProvider * @param serverCodecConfigurer * @return */ @Primary @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public ErrorWebExceptionHandler errorWebExceptionHandler(ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) { JsonExceptionHandler jsonExceptionHandler = new JsonExceptionHandler(); jsonExceptionHandler.setViewResolvers(viewResolversProvider.getIfAvailable(Collections::emptyList)); jsonExceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters()); jsonExceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders()); return jsonExceptionHandler; }
这样当304返回空body的时候就直接对body进行赋值,从而使得浏览器正常304;但是如果加上了熔断后(熔断的优先级高于全局异常),会导致全局异常不起作用直接熔断,于是我在熔断代码里加了以下代码:
@RequestMapping("/fallbackSimple") @ResponseBody public LinkedHashMap<String, Object> fallbackSimple(ServerWebExchange exchange) { ServerHttpResponse response = exchange.getResponse(); DataBufferFactory bufferFactory = response.bufferFactory(); bufferFactory.allocateBuffer().read();//读取返回流的时候会异常,从而进入全局异常,全局异常根据文件后缀设置状态码 log.info("服务熔断了----------fallbackSimple"); return fallback; }
这样既能使用全局异常,又能使用熔断;
以上方案为一个取巧方案;
还有另一个方案就是修改gateway的源代码重新打包(这种方案本人没有实际测试过,debug调试下看了可以获取到304状态码)
状态码那一行提前,然后进行状态码判断,理论上这种方案更科学;