- The implementation leverages Sentinel Reactor Adapter. Two main components: SentinelWebFluxFilter and SentinelBlockExceptionHandler Signed-off-by: Eric Zhao <sczyh16@gmail.com>master
@@ -20,6 +20,7 @@ | |||||
<module>sentinel-grpc-adapter</module> | <module>sentinel-grpc-adapter</module> | ||||
<module>sentinel-zuul-adapter</module> | <module>sentinel-zuul-adapter</module> | ||||
<module>sentinel-reactor-adapter</module> | <module>sentinel-reactor-adapter</module> | ||||
<module>sentinel-spring-webflux-adapter</module> | |||||
</modules> | </modules> | ||||
<dependencyManagement> | <dependencyManagement> | ||||
@@ -39,6 +40,11 @@ | |||||
<artifactId>sentinel-web-servlet</artifactId> | <artifactId>sentinel-web-servlet</artifactId> | ||||
<version>${project.version}</version> | <version>${project.version}</version> | ||||
</dependency> | </dependency> | ||||
<dependency> | |||||
<groupId>com.alibaba.csp</groupId> | |||||
<artifactId>sentinel-reactor-adapter</artifactId> | |||||
<version>${project.version}</version> | |||||
</dependency> | |||||
<dependency> | <dependency> | ||||
<groupId>junit</groupId> | <groupId>junit</groupId> | ||||
@@ -0,0 +1,55 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project xmlns="http://maven.apache.org/POM/4.0.0" | |||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | |||||
<parent> | |||||
<artifactId>sentinel-adapter</artifactId> | |||||
<groupId>com.alibaba.csp</groupId> | |||||
<version>1.5.0-SNAPSHOT</version> | |||||
</parent> | |||||
<modelVersion>4.0.0</modelVersion> | |||||
<artifactId>sentinel-spring-webflux-adapter</artifactId> | |||||
<properties> | |||||
<java.source.version>1.8</java.source.version> | |||||
<java.target.version>1.8</java.target.version> | |||||
<spring.version>5.1.5.RELEASE</spring.version> | |||||
<spring.boot.version>2.1.3.RELEASE</spring.boot.version> | |||||
</properties> | |||||
<dependencies> | |||||
<dependency> | |||||
<groupId>com.alibaba.csp</groupId> | |||||
<artifactId>sentinel-core</artifactId> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>com.alibaba.csp</groupId> | |||||
<artifactId>sentinel-reactor-adapter</artifactId> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>org.springframework</groupId> | |||||
<artifactId>spring-webflux</artifactId> | |||||
<version>${spring.version}</version> | |||||
<scope>provided</scope> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>junit</groupId> | |||||
<artifactId>junit</artifactId> | |||||
<scope>test</scope> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>org.springframework.boot</groupId> | |||||
<artifactId>spring-boot-starter-webflux</artifactId> | |||||
<version>${spring.boot.version}</version> | |||||
<scope>test</scope> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>org.springframework.boot</groupId> | |||||
<artifactId>spring-boot-starter-test</artifactId> | |||||
<version>${spring.boot.version}</version> | |||||
<scope>test</scope> | |||||
</dependency> | |||||
</dependencies> | |||||
</project> |
@@ -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<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { | |||||
return chain.filter(exchange) | |||||
.transform(buildSentinelTransformer(exchange)); | |||||
} | |||||
private SentinelReactorTransformer<Void> 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 = ""; | |||||
} |
@@ -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<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t); | |||||
} |
@@ -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<ServerResponse> 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<ServerResponse> 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<MediaType> 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; | |||||
} | |||||
} | |||||
} |
@@ -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<ServerWebExchange, String, String> DEFAULT_URL_CLEANER = (w, url) -> url; | |||||
private static final Function<ServerWebExchange, String> DEFAULT_ORIGIN_PARSER = (w) -> ""; | |||||
/** | |||||
* BlockRequestHandler: (serverExchange, exception) -> response | |||||
*/ | |||||
private static volatile BlockRequestHandler blockHandler = new DefaultBlockRequestHandler(); | |||||
/** | |||||
* UrlCleaner: (serverExchange, originalUrl) -> finalUrl | |||||
*/ | |||||
private static volatile BiFunction<ServerWebExchange, String, String> urlCleaner = DEFAULT_URL_CLEANER; | |||||
/** | |||||
* RequestOriginParser: (serverExchange) -> origin | |||||
*/ | |||||
private static volatile Function<ServerWebExchange, String> 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<ServerWebExchange, String, String> getUrlCleaner() { | |||||
return urlCleaner; | |||||
} | |||||
public static void resetUrlCleaner() { | |||||
WebFluxCallbackManager.urlCleaner = DEFAULT_URL_CLEANER; | |||||
} | |||||
public static void setUrlCleaner(BiFunction<ServerWebExchange, String, String> urlCleaner) { | |||||
AssertUtil.notNull(urlCleaner, "urlCleaner cannot be null"); | |||||
WebFluxCallbackManager.urlCleaner = urlCleaner; | |||||
} | |||||
public static /*@NonNull*/ Function<ServerWebExchange, String> getRequestOriginParser() { | |||||
return requestOriginParser; | |||||
} | |||||
public static void resetRequestOriginParser() { | |||||
WebFluxCallbackManager.requestOriginParser = DEFAULT_ORIGIN_PARSER; | |||||
} | |||||
public static void setRequestOriginParser(Function<ServerWebExchange, String> requestOriginParser) { | |||||
AssertUtil.notNull(requestOriginParser, "requestOriginParser cannot be null"); | |||||
WebFluxCallbackManager.requestOriginParser = requestOriginParser; | |||||
} | |||||
private WebFluxCallbackManager() {} | |||||
} |
@@ -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<ViewResolver> viewResolvers; | |||||
private List<HttpMessageWriter<?>> messageWriters; | |||||
public SentinelBlockExceptionHandler(List<ViewResolver> viewResolvers, ServerCodecConfigurer serverCodecConfigurer) { | |||||
this.viewResolvers = viewResolvers; | |||||
this.messageWriters = serverCodecConfigurer.getWriters(); | |||||
} | |||||
private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange) { | |||||
return response.writeTo(exchange, contextSupplier.get()); | |||||
} | |||||
@Override | |||||
public Mono<Void> 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<ServerResponse> handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) { | |||||
return WebFluxCallbackManager.getBlockHandler().handleRequest(exchange, throwable); | |||||
} | |||||
private final Supplier<ServerResponse.Context> contextSupplier = () -> new ServerResponse.Context() { | |||||
@Override | |||||
public List<HttpMessageWriter<?>> messageWriters() { | |||||
return SentinelBlockExceptionHandler.this.messageWriters; | |||||
} | |||||
@Override | |||||
public List<ViewResolver> viewResolvers() { | |||||
return SentinelBlockExceptionHandler.this.viewResolvers; | |||||
} | |||||
}; | |||||
} |