From d481014d8acabd56a9aa17cecdc4e703a4d40da2 Mon Sep 17 00:00:00 2001 From: Eric Zhao Date: Fri, 8 Mar 2019 14:27:44 +0800 Subject: [PATCH] Add adapter module and implementation for Spring WebFlux - The implementation leverages Sentinel Reactor Adapter. Two main components: SentinelWebFluxFilter and SentinelBlockExceptionHandler Signed-off-by: Eric Zhao --- sentinel-adapter/pom.xml | 6 ++ .../sentinel-spring-webflux-adapter/pom.xml | 55 +++++++++++ .../spring/webflux/SentinelWebFluxFilter.java | 60 ++++++++++++ .../webflux/callback/BlockRequestHandler.java | 39 ++++++++ .../callback/DefaultBlockRequestHandler.java | 93 +++++++++++++++++++ .../callback/WebFluxCallbackManager.java | 87 +++++++++++++++++ .../SentinelBlockExceptionHandler.java | 78 ++++++++++++++++ 7 files changed, 418 insertions(+) create mode 100644 sentinel-adapter/sentinel-spring-webflux-adapter/pom.xml create mode 100644 sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/SentinelWebFluxFilter.java create mode 100644 sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/callback/BlockRequestHandler.java create mode 100644 sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/callback/DefaultBlockRequestHandler.java create mode 100644 sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/callback/WebFluxCallbackManager.java create mode 100644 sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/exception/SentinelBlockExceptionHandler.java diff --git a/sentinel-adapter/pom.xml b/sentinel-adapter/pom.xml index 39d7e496..15d27657 100755 --- a/sentinel-adapter/pom.xml +++ b/sentinel-adapter/pom.xml @@ -20,6 +20,7 @@ sentinel-grpc-adapter sentinel-zuul-adapter sentinel-reactor-adapter + sentinel-spring-webflux-adapter @@ -39,6 +40,11 @@ sentinel-web-servlet ${project.version} + + com.alibaba.csp + sentinel-reactor-adapter + ${project.version} + junit diff --git a/sentinel-adapter/sentinel-spring-webflux-adapter/pom.xml b/sentinel-adapter/sentinel-spring-webflux-adapter/pom.xml new file mode 100644 index 00000000..2231532c --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webflux-adapter/pom.xml @@ -0,0 +1,55 @@ + + + + sentinel-adapter + com.alibaba.csp + 1.5.0-SNAPSHOT + + 4.0.0 + + sentinel-spring-webflux-adapter + + + 1.8 + 1.8 + 5.1.5.RELEASE + 2.1.3.RELEASE + + + + + com.alibaba.csp + sentinel-core + + + com.alibaba.csp + sentinel-reactor-adapter + + + org.springframework + spring-webflux + ${spring.version} + provided + + + + junit + junit + test + + + org.springframework.boot + spring-boot-starter-webflux + ${spring.boot.version} + test + + + org.springframework.boot + spring-boot-starter-test + ${spring.boot.version} + test + + + \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/SentinelWebFluxFilter.java b/sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/SentinelWebFluxFilter.java new file mode 100644 index 00000000..b83767d2 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/SentinelWebFluxFilter.java @@ -0,0 +1,60 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.adapter.spring.webflux; + +import java.util.Optional; + +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.adapter.reactor.ContextConfig; +import com.alibaba.csp.sentinel.adapter.reactor.EntryConfig; +import com.alibaba.csp.sentinel.adapter.reactor.SentinelReactorTransformer; +import com.alibaba.csp.sentinel.adapter.spring.webflux.callback.WebFluxCallbackManager; + +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +/** + * @author Eric Zhao + * @since 1.5.0 + */ +public class SentinelWebFluxFilter implements WebFilter { + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return chain.filter(exchange) + .transform(buildSentinelTransformer(exchange)); + } + + private SentinelReactorTransformer buildSentinelTransformer(ServerWebExchange exchange) { + // Maybe we can get the URL pattern elsewhere via: + // exchange.getAttributeOrDefault(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, path) + + String path = exchange.getRequest().getPath().value(); + String finalPath = Optional.ofNullable(WebFluxCallbackManager.getUrlCleaner()) + .map(f -> f.apply(exchange, path)) + .orElse(path); + String origin = Optional.ofNullable(WebFluxCallbackManager.getRequestOriginParser()) + .map(f -> f.apply(exchange)) + .orElse(EMPTY_ORIGIN); + + return new SentinelReactorTransformer<>( + new EntryConfig(finalPath, EntryType.IN, new ContextConfig(finalPath, origin))); + } + + private static final String EMPTY_ORIGIN = ""; +} diff --git a/sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/callback/BlockRequestHandler.java b/sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/callback/BlockRequestHandler.java new file mode 100644 index 00000000..bc64f7bf --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/callback/BlockRequestHandler.java @@ -0,0 +1,39 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.adapter.spring.webflux.callback; + +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +/** + * Reactive handler for the blocked request. + * + * @author Eric Zhao + * @since 1.5.0 + */ +@FunctionalInterface +public interface BlockRequestHandler { + + /** + * Handle the blocked request. + * + * @param exchange server exchange object + * @param t block exception + * @return server response to return + */ + Mono handleRequest(ServerWebExchange exchange, Throwable t); +} diff --git a/sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/callback/DefaultBlockRequestHandler.java b/sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/callback/DefaultBlockRequestHandler.java new file mode 100644 index 00000000..58d6ecbf --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/callback/DefaultBlockRequestHandler.java @@ -0,0 +1,93 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.adapter.spring.webflux.callback; + +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.http.InvalidMediaTypeException; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import static org.springframework.web.reactive.function.BodyInserters.fromObject; + +/** + * The default implementation of {@link BlockRequestHandler}. + * + * @author Eric Zhao + * @since 1.5.0 + */ +public class DefaultBlockRequestHandler implements BlockRequestHandler { + + private static final String DEFAULT_BLOCK_MSG_PREFIX = "Blocked by Sentinel: "; + + @Override + public Mono handleRequest(ServerWebExchange exchange, Throwable ex) { + if (acceptsHtml(exchange)) { + return htmlErrorResponse(ex); + } + // JSON result by default. + return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS) + .contentType(MediaType.APPLICATION_JSON_UTF8) + .body(fromObject(buildErrorResult(ex))); + } + + private Mono htmlErrorResponse(Throwable ex) { + return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS) + .contentType(MediaType.TEXT_PLAIN) + .syncBody(DEFAULT_BLOCK_MSG_PREFIX + ex.getClass().getSimpleName()); + } + + private ErrorResult buildErrorResult(Throwable ex) { + return new ErrorResult(HttpStatus.TOO_MANY_REQUESTS.value(), + DEFAULT_BLOCK_MSG_PREFIX + ex.getClass().getSimpleName()); + } + + /** + * Reference from {@code DefaultErrorWebExceptionHandler} of Spring Boot. + */ + private boolean acceptsHtml(ServerWebExchange exchange) { + try { + List acceptedMediaTypes = exchange.getRequest().getHeaders().getAccept(); + acceptedMediaTypes.remove(MediaType.ALL); + MediaType.sortBySpecificityAndQuality(acceptedMediaTypes); + return acceptedMediaTypes.stream() + .anyMatch(MediaType.TEXT_HTML::isCompatibleWith); + } catch (InvalidMediaTypeException ex) { + return false; + } + } + + private static class ErrorResult { + private final int code; + private final String message; + + ErrorResult(int code, String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } + } +} diff --git a/sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/callback/WebFluxCallbackManager.java b/sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/callback/WebFluxCallbackManager.java new file mode 100644 index 00000000..9acf7998 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/callback/WebFluxCallbackManager.java @@ -0,0 +1,87 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.adapter.spring.webflux.callback; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import com.alibaba.csp.sentinel.util.AssertUtil; + +import org.springframework.web.server.ServerWebExchange; + +/** + * @author Eric Zhao + * @since 1.5.0 + */ +public final class WebFluxCallbackManager { + + private static final BiFunction DEFAULT_URL_CLEANER = (w, url) -> url; + private static final Function DEFAULT_ORIGIN_PARSER = (w) -> ""; + + /** + * BlockRequestHandler: (serverExchange, exception) -> response + */ + private static volatile BlockRequestHandler blockHandler = new DefaultBlockRequestHandler(); + /** + * UrlCleaner: (serverExchange, originalUrl) -> finalUrl + */ + private static volatile BiFunction urlCleaner = DEFAULT_URL_CLEANER; + /** + * RequestOriginParser: (serverExchange) -> origin + */ + private static volatile Function requestOriginParser = DEFAULT_ORIGIN_PARSER; + + public static /*@NonNull*/ BlockRequestHandler getBlockHandler() { + return blockHandler; + } + + public static void resetBlockHandler() { + WebFluxCallbackManager.blockHandler = new DefaultBlockRequestHandler(); + } + + public static void setBlockHandler(BlockRequestHandler blockHandler) { + AssertUtil.notNull(blockHandler, "blockHandler cannot be null"); + WebFluxCallbackManager.blockHandler = blockHandler; + } + + public static /*@NonNull*/ BiFunction getUrlCleaner() { + return urlCleaner; + } + + public static void resetUrlCleaner() { + WebFluxCallbackManager.urlCleaner = DEFAULT_URL_CLEANER; + } + + public static void setUrlCleaner(BiFunction urlCleaner) { + AssertUtil.notNull(urlCleaner, "urlCleaner cannot be null"); + WebFluxCallbackManager.urlCleaner = urlCleaner; + } + + public static /*@NonNull*/ Function getRequestOriginParser() { + return requestOriginParser; + } + + public static void resetRequestOriginParser() { + WebFluxCallbackManager.requestOriginParser = DEFAULT_ORIGIN_PARSER; + } + + public static void setRequestOriginParser(Function requestOriginParser) { + AssertUtil.notNull(requestOriginParser, "requestOriginParser cannot be null"); + WebFluxCallbackManager.requestOriginParser = requestOriginParser; + } + + private WebFluxCallbackManager() {} +} diff --git a/sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/exception/SentinelBlockExceptionHandler.java b/sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/exception/SentinelBlockExceptionHandler.java new file mode 100644 index 00000000..a9a35c71 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-webflux-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/spring/webflux/exception/SentinelBlockExceptionHandler.java @@ -0,0 +1,78 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.adapter.spring.webflux.exception; + +import java.util.List; + +import com.alibaba.csp.sentinel.adapter.spring.webflux.callback.WebFluxCallbackManager; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.util.function.Supplier; + +import org.springframework.http.codec.HttpMessageWriter; +import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebExceptionHandler; +import reactor.core.publisher.Mono; + +/** + * @author Eric Zhao + * @since 1.5.0 + */ +public class SentinelBlockExceptionHandler implements WebExceptionHandler { + + private List viewResolvers; + private List> messageWriters; + + public SentinelBlockExceptionHandler(List viewResolvers, ServerCodecConfigurer serverCodecConfigurer) { + this.viewResolvers = viewResolvers; + this.messageWriters = serverCodecConfigurer.getWriters(); + } + + private Mono writeResponse(ServerResponse response, ServerWebExchange exchange) { + return response.writeTo(exchange, contextSupplier.get()); + } + + @Override + public Mono handle(ServerWebExchange exchange, Throwable ex) { + if (exchange.getResponse().isCommitted()) { + return Mono.error(ex); + } + // This exception handler only handles rejection by Sentinel. + if (!BlockException.isBlockException(ex)) { + return Mono.error(ex); + } + return handleBlockedRequest(exchange, ex) + .flatMap(response -> writeResponse(response, exchange)); + } + + private Mono handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) { + return WebFluxCallbackManager.getBlockHandler().handleRequest(exchange, throwable); + } + + private final Supplier contextSupplier = () -> new ServerResponse.Context() { + @Override + public List> messageWriters() { + return SentinelBlockExceptionHandler.this.messageWriters; + } + + @Override + public List viewResolvers() { + return SentinelBlockExceptionHandler.this.viewResolvers; + } + }; +}