Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support spring boot 3.x #3348

Closed
wants to merge 3 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -33,8 +33,13 @@ jobs:
java-version: 17
distribution: 'temurin'

- name: Test with Maven
- name: Maven Test With Spring 6.x
run: mvn --batch-mode test -Dsurefire.jdk-toolchain-version=${{ matrix.java }}
if: ${{ matrix.java >= 17 }}

- name: Maven Test Without Spring 6.x
run: mvn --batch-mode test -Dsurefire.jdk-toolchain-version=${{ matrix.java }} -Dskip.spring.v6x.test=true
if: ${{ matrix.java < 17 }}

- name: Build with Maven
run: mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V -DminimumPriority=1
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -159,6 +159,13 @@
<artifactId>sentinel-cluster-common-default</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-web-adapter-common</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-adapter</artifactId>
2 changes: 2 additions & 0 deletions sentinel-adapter/pom.xml
Original file line number Diff line number Diff line change
@@ -27,7 +27,9 @@
<module>sentinel-spring-webflux-adapter</module>
<module>sentinel-api-gateway-adapter-common</module>
<module>sentinel-spring-cloud-gateway-adapter</module>
<module>sentinel-web-adapter-common</module>
<module>sentinel-spring-webmvc-adapter</module>
<module>sentinel-spring-webmvc-v6x-adapter</module>
<module>sentinel-zuul2-adapter</module>
<module>sentinel-okhttp-adapter</module>
<module>sentinel-jax-rs-adapter</module>
Original file line number Diff line number Diff line change
@@ -25,6 +25,8 @@

/**
* Spring Web MVC interceptor that integrates with Sentinel.
* <p>
* This will record resource as `${uri}`.
*
* @author kaizi2009
* @since 1.7.1
107 changes: 107 additions & 0 deletions sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Sentinel Spring MVC Adapter

## Introduction

Sentinel provides integration for Spring Web to enable flow control for web requests.

Add the following dependency in `pom.xml` (if you are using Maven):

```xml
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-webmvc-v6x-adapter</artifactId>
<version>x.y.z</version>
</dependency>
```

Then we could add a configuration bean to configure the interceptor:

```java
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
SentinelWebMvcConfig config = new SentinelWebMvcConfig();
// Enable the HTTP method prefix.
config.setHttpMethodSpecify(true);
// Add to the interceptor list.
registry.addInterceptor(new SentinelWebInterceptor(config)).addPathPatterns("/**");
}
}
```

Then Sentinel will extract URL patterns defined in Web Controller as the web resource (e.g. `/foo/{id}`).

## Configuration

### Block handling

Sentinel Spring Web adapter provides a `BlockExceptionHandler` interface to handle the blocked requests.
We could set the handler via `SentinelWebMvcTotalConfig#setBlockExceptionHandler()` method.

By default the interceptor will throw out the `BlockException`.
We need to set a global exception handler function in Spring to handle it. An example:

```java
@ControllerAdvice
@Order(0)
public class SentinelBlockExceptionHandlerConfig {
private Logger logger = LoggerFactory.getLogger(this.getClass());

@ExceptionHandler(BlockException.class)
@ResponseBody
public String sentinelBlockHandler(BlockException e) {
AbstractRule rule = e.getRule();
logger.info("Blocked by Sentinel: {}", rule.toString());
return "Blocked by Sentinel";
}
}
```

We've provided a `DefaultBlockExceptionHandler`. When a request is blocked, the handler will return a default page
indicating the request is rejected (`Blocked by Sentinel (flow limiting)`).
The HTTP status code of the default block page is **429 (Too Many Requests)**.

We could also implement our implementation of the `BlockExceptionHandler` interface and
set to the config object. An example:

```java
SentinelWebMvcConfig config = new SentinelWebMvcConfig();
config.setBlockExceptionHandler((request, response, e) -> {
String resourceName = e.getRule().getResource();
// Depending on your situation, you can choose to process or throw
if ("/hello".equals(resourceName)) {
// Do something ......
response.getWriter().write("Blocked by Sentinel");
} else {
// Handle it in global exception handling
throw e;
}
});
```

### Customized configuration

- Common configuration in `SentinelWebMvcConfig` and `SentinelWebMvcTotalConfig`:

| name | description | type | default value |
|------|------------|------|-------|
| `blockExceptionHandler`| The handler that handles the block request | `BlockExceptionHandler` | null (throw out the BlockException) |
| `originParser` | Extracting request origin (e.g. IP or appName from HTTP Header) from HTTP request | `RequestOriginParser` | - |

- `SentinelWebMvcConfig` configuration:

| name | description | type | default value |
|------|------------|------|-------|
| urlCleaner | The `UrlCleaner` interface is designed for clean and unify the URL resource. | `UrlCleaner` | - |
| requestAttributeName | Attribute key in request used by Sentinel (internal) | `String` | `$$sentinel_spring_web_entry_attr` |
| httpMethodSpecify | Specify whether the URL resource name should contain the HTTP method prefix (e.g. `POST:`). | `boolean` | `false` |
| webContextUnify | Specify whether unify web context(i.e. use the default context name). | `boolean` | `true` |

- `SentinelWebMvcTotalConfig` configuration:

| name | description | type | default value |
|------|------------|------|-------|
| totalResourceName | The resource name in `SentinelTotalInterceptor` | `String` | `spring-mvc-total-url-request` |
| requestAttributeName | Attribute key in request used by Sentinel (internal) | `String` | `$$sentinel_spring_web_total_entry_attr` |
121 changes: 121 additions & 0 deletions sentinel-adapter/sentinel-spring-webmvc-v6x-adapter/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?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>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-adapter</artifactId>
<version>1.8.7</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>sentinel-spring-webmvc-v6x-adapter</artifactId>
<packaging>jar</packaging>

<properties>
<spring.version>6.0.2</spring.version>
<spring.boot.version>3.0.0</spring.boot.version>
<servlet.api.version>6.0.0</servlet.api.version>
<jakarta.xml.bind-api.version>4.0.0</jakarta.xml.bind-api.version>
<slf4j-api.version>2.0.4</slf4j-api.version>

<skip.spring.v6x.test>false</skip.spring.v6x.test>
</properties>

<dependencies>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-web-adapter-common</artifactId>
</dependency>

<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>${servlet.api.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>${jakarta.xml.bind-api.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j-api.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven.surefire.version}</version>
<dependencies>
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-junit47</artifactId>
<version>3.2.5</version>
</dependency>
</dependencies>
<configuration>
<skipTests>${skip.spring.v6x.test}</skipTests>
</configuration>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/*
* Copyright 1999-2024 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
*
* https://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.webmvc_v6x;

import com.alibaba.csp.sentinel.*;
import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.config.BaseWebMvcConfig;
import com.alibaba.csp.sentinel.context.ContextUtil;
import com.alibaba.csp.sentinel.log.RecordLog;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.csp.sentinel.util.StringUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

/**
* Since request may be reprocessed in flow if any forwarding or including or other action
* happened (see {@link jakarta.servlet.ServletRequest#getDispatcherType()}) we will only
* deal with the initial request. So we use <b>reference count</b> to track in
* dispatching "onion" though which we could figure out whether we are in initial type "REQUEST".
* That means the sub-requests which we rarely meet in practice will NOT be recorded in Sentinel.
* <p>
* How to implement a forward sub-request in your action:
* <pre>
* initialRequest() {
* ModelAndView mav = new ModelAndView();
* mav.setViewName("another");
* return mav;
* }
* </pre>
*
* @since 1.8.8
*/
public abstract class AbstractSentinelInterceptor implements HandlerInterceptor {

public static final String SENTINEL_SPRING_WEB_CONTEXT_NAME = "sentinel_spring_web_context";
private static final String EMPTY_ORIGIN = "";

private final BaseWebMvcConfig baseWebMvcConfig;

public AbstractSentinelInterceptor(BaseWebMvcConfig config) {
AssertUtil.notNull(config, "BaseWebMvcConfig should not be null");
AssertUtil.assertNotBlank(config.getRequestAttributeName(), "requestAttributeName should not be blank");
this.baseWebMvcConfig = config;
}

/**
* @param request
* @param rcKey
* @param step
* @return reference count after increasing (initial value as zero to be increased)
*/
private Integer increaseReference(HttpServletRequest request, String rcKey, int step) {
Object obj = request.getAttribute(rcKey);

if (obj == null) {
// initial
obj = Integer.valueOf(0);
}

Integer newRc = (Integer) obj + step;
request.setAttribute(rcKey, newRc);
return newRc;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String resourceName = "";
try {
resourceName = getResourceName(request);
if (StringUtil.isEmpty(resourceName)) {
return true;
}
if (increaseReference(request, this.baseWebMvcConfig.getRequestRefName(), 1) != 1) {
return true;
}
// Parse the request origin using registered origin parser.
String origin = parseOrigin(request);
String contextName = getContextName(request);
ContextUtil.enter(contextName, origin);
Entry entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
request.setAttribute(baseWebMvcConfig.getRequestAttributeName(), entry);
return true;
} catch (BlockException e) {
try {
handleBlockException(request, response, resourceName, e);
} finally {
ContextUtil.exit();
}
return false;
}
}

/**
* Return the resource name of the target web resource.
*
* @param request web request
* @return the resource name of the target web resource.
*/
protected abstract String getResourceName(HttpServletRequest request);

/**
* Return the context name of the target web resource.
*
* @param request web request
* @return the context name of the target web resource.
*/
protected String getContextName(HttpServletRequest request) {
return SENTINEL_SPRING_WEB_CONTEXT_NAME;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
if (increaseReference(request, this.baseWebMvcConfig.getRequestRefName(), -1) != 0) {
return;
}

Entry entry = getEntryInRequest(request, baseWebMvcConfig.getRequestAttributeName());
if (entry == null) {
// should not happen
RecordLog.warn("[{}] No entry found in request, key: {}",
getClass().getSimpleName(), baseWebMvcConfig.getRequestAttributeName());
return;
}

// Record the status code here.
// String resourceName = entry.getResourceWrapper().getName();
// int status = response.getStatus();
// StatusCodeMetricManager.getInstance().recordStatusCode(resourceName, status);

traceExceptionAndExit(entry, ex);
removeEntryInRequest(request);
ContextUtil.exit();
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
}

protected Entry getEntryInRequest(HttpServletRequest request, String attrKey) {
Object entryObject = request.getAttribute(attrKey);
return entryObject == null ? null : (Entry) entryObject;
}

protected void removeEntryInRequest(HttpServletRequest request) {
request.removeAttribute(baseWebMvcConfig.getRequestAttributeName());
}

protected void traceExceptionAndExit(Entry entry, Exception ex) {
if (entry != null) {
if (ex != null) {
Tracer.traceEntry(ex, entry);
}
entry.exit();
}
}

protected void handleBlockException(HttpServletRequest request, HttpServletResponse response, String resourceName,
BlockException e)
throws Exception {
if (baseWebMvcConfig.getBlockExceptionHandler() != null) {
baseWebMvcConfig.getBlockExceptionHandler().handle(request, response, resourceName, e);

// Record status when blocked
// int status = response.getStatus();
// StatusCodeMetricManager.getInstance().recordStatusCode(resourceName, status);
} else {
// Throw BlockException directly. Users need to handle it in Spring global exception handler.
// NOTE: the status code statistics will be lost here!
throw e;
}
}

protected String parseOrigin(HttpServletRequest request) {
String origin = EMPTY_ORIGIN;
if (baseWebMvcConfig.getOriginParser() != null) {
origin = baseWebMvcConfig.getOriginParser().parseOrigin(request);
if (StringUtil.isEmpty(origin)) {
return EMPTY_ORIGIN;
}
}
return origin;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 1999-2024 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
*
* https://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.webmvc_v6x;

import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.config.SentinelWebMvcConfig;

import com.alibaba.csp.sentinel.adapter.web.common.UrlCleaner;
import jakarta.servlet.http.HttpServletRequest;

import org.springframework.web.servlet.HandlerMapping;

/**
* Spring Web MVC interceptor that integrates with Sentinel.
* <p>
* This will record resource as `${uri}`.
*
* @since 1.8.8
*/
public class SentinelWebInterceptor extends AbstractSentinelInterceptor {

private final SentinelWebMvcConfig config;

public SentinelWebInterceptor() {
this(new SentinelWebMvcConfig());
}

public SentinelWebInterceptor(SentinelWebMvcConfig config) {
super(config);
if (config == null) {
// Use the default config by default.
this.config = new SentinelWebMvcConfig();
} else {
this.config = config;
}
}

@Override
protected String getResourceName(HttpServletRequest request) {
// Resolve the Spring Web URL pattern from the request attribute.
Object resourceNameObject = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
if (resourceNameObject == null || !(resourceNameObject instanceof String)) {
return null;
}
String resourceName = (String) resourceNameObject;
UrlCleaner urlCleaner = config.getUrlCleaner();
if (urlCleaner != null) {
resourceName = urlCleaner.clean(resourceName);
}
if (config.isContextPathSpecify() && request.getContextPath() != null) {
resourceName = request.getContextPath() + resourceName;
}
return resourceName;
}

@Override
protected String getContextName(HttpServletRequest request) {
if (config.isWebContextUnify()) {
return super.getContextName(request);
}

return getResourceName(request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 1999-2024 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
*
* https://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.webmvc_v6x;

import com.alibaba.csp.sentinel.util.StringUtil;
import jakarta.servlet.http.HttpServletRequest;

/**
* Spring Web MVC interceptor that integrates with Sentinel.
* <p>
* This will record resource as `${httpMethod}:${uri}`.
*
* @since 1.8.8
*/
public class SentinelWebPrefixInterceptor extends SentinelWebInterceptor {

@Override
protected String getResourceName(HttpServletRequest request) {
String resourceName = super.getResourceName(request);
// Add method specification
if (StringUtil.isNotEmpty(resourceName)) {
resourceName = request.getMethod().toUpperCase() + ":" + resourceName;
}
return resourceName;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 1999-2024 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
*
* https://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.webmvc_v6x;

import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.config.SentinelWebMvcTotalConfig;

import jakarta.servlet.http.HttpServletRequest;

/**
* The web interceptor for all requests, which will unify all URL as
* a single resource name (configured in {@link SentinelWebMvcTotalConfig}).
*
* @since 1.8.8
*/
public class SentinelWebTotalInterceptor extends AbstractSentinelInterceptor {

private final SentinelWebMvcTotalConfig config;

public SentinelWebTotalInterceptor(SentinelWebMvcTotalConfig config) {
super(config);
if (config == null) {
this.config = new SentinelWebMvcTotalConfig();
} else {
this.config = config;
}
}

public SentinelWebTotalInterceptor() {
this(new SentinelWebMvcTotalConfig());
}

@Override
protected String getResourceName(HttpServletRequest request) {
return config.getTotalResourceName();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 1999-2024 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
*
* https://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.webmvc_v6x.callback;

import com.alibaba.csp.sentinel.slots.block.BlockException;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
* Handler for the blocked request.
*
* @since 1.8.8
*/
public interface BlockExceptionHandler {

/**
* Handle the request when blocked.
*
* @param request Servlet request
* @param response Servlet response
* @param resourceName resource name
* @param e the block exception
* @throws Exception users may throw out the BlockException or other error occurs
*/
void handle(HttpServletRequest request, HttpServletResponse response, String resourceName, BlockException e)
throws Exception;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 1999-2024 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
*
* https://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.webmvc_v6x.callback;

import com.alibaba.csp.sentinel.slots.block.BlockException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.PrintWriter;

/**
* Default handler for the blocked request.
*
* @since 1.8.8
*/
public class DefaultBlockExceptionHandler implements BlockExceptionHandler {

@Override
public void handle(HttpServletRequest request, HttpServletResponse response, String resourceName, BlockException ex)
throws Exception {
// Return 429 (Too Many Requests) by default.
response.setStatus(429);

PrintWriter out = response.getWriter();
out.print("Blocked by Sentinel (flow limiting)");
out.flush();
out.close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 1999-2024 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.webmvc_v6x.callback;

import jakarta.servlet.http.HttpServletRequest;

/**
* The origin parser parses request origin (e.g. IP, user, appName) from HTTP request.
*
* @since 1.8.8
*/
public interface RequestOriginParser {

/**
* Parse the origin from given HTTP request.
*
* @param request HTTP request
* @return parsed origin
*/
String parseOrigin(HttpServletRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 1999-2024 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.webmvc_v6x.config;

import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback.DefaultBlockExceptionHandler;
import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback.RequestOriginParser;

/**
* Common base configuration for Spring Web MVC adapter.
*
* @since 1.8.8
*/
public abstract class BaseWebMvcConfig {

protected String requestAttributeName;
protected String requestRefName;
protected BlockExceptionHandler blockExceptionHandler = new DefaultBlockExceptionHandler();
protected RequestOriginParser originParser;

public String getRequestAttributeName() {
return requestAttributeName;
}

public void setRequestAttributeName(String requestAttributeName) {
this.requestAttributeName = requestAttributeName;
this.requestRefName = this.requestAttributeName + "-rc";
}

/**
* Paired with attr name used to track reference count.
*
* @return
*/
public String getRequestRefName() {
return requestRefName;
}

public BlockExceptionHandler getBlockExceptionHandler() {
return blockExceptionHandler;
}

public void setBlockExceptionHandler(BlockExceptionHandler blockExceptionHandler) {
this.blockExceptionHandler = blockExceptionHandler;
}

public RequestOriginParser getOriginParser() {
return originParser;
}

public void setOriginParser(RequestOriginParser originParser) {
this.originParser = originParser;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.config;

import com.alibaba.csp.sentinel.adapter.web.common.UrlCleaner;

/**
* @since 1.8.8
*/
public class SentinelPreWebMvcConfig extends BaseWebMvcConfig {

public static final String DEFAULT_REQUEST_ATTRIBUTE_NAME = "$$sentinel_pre_spring_web_entry_attr";

private UrlCleaner urlCleaner;

/**
* Specify whether the URL resource name should contain the HTTP method prefix (e.g. {@code POST:}).
*/
private boolean httpMethodSpecify;

/**
* Specify whether unify web context(i.e. use the default context name), and is true by default.
*
* @since 1.7.2
*/
private boolean webContextUnify = true;

public SentinelPreWebMvcConfig() {
super();
setRequestAttributeName(DEFAULT_REQUEST_ATTRIBUTE_NAME);
}

public boolean isHttpMethodSpecify() {
return httpMethodSpecify;
}

public SentinelPreWebMvcConfig setHttpMethodSpecify(boolean httpMethodSpecify) {
this.httpMethodSpecify = httpMethodSpecify;
return this;
}

public boolean isWebContextUnify() {
return webContextUnify;
}

public SentinelPreWebMvcConfig setWebContextUnify(boolean webContextUnify) {
this.webContextUnify = webContextUnify;
return this;
}

public UrlCleaner getUrlCleaner() {
return urlCleaner;
}

public SentinelPreWebMvcConfig setUrlCleaner(UrlCleaner urlCleaner) {
this.urlCleaner = urlCleaner;
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright 1999-2024 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.webmvc_v6x.config;

import com.alibaba.csp.sentinel.adapter.web.common.UrlCleaner;

/**
* @since 1.8.8
*/
public class SentinelWebMvcConfig extends BaseWebMvcConfig {

public static final String DEFAULT_REQUEST_ATTRIBUTE_NAME = "$$sentinel_spring_web_entry_attr";

/**
* Specify the URL cleaner that unifies the URL resources.
*/
private UrlCleaner urlCleaner;

/**
* Specify whether the URL resource name should contain the HTTP method prefix (e.g. {@code POST:}).
*/
private boolean httpMethodSpecify;

/**
* Specify whether the URL resource name should contain the HTTP method prefix in MSE (e.g. {@code GET:}).
*/
private static boolean mseHttpMethodSpecify;


/**
* Specify whether unify web context(i.e. use the default context name), and is true by default.
*
* @since 1.7.2
*/
private boolean webContextUnify = true;

/**
* Specify whether the URL resource name should contain the context-path
*/
private boolean contextPathSpecify = true;

public SentinelWebMvcConfig() {
super();
setRequestAttributeName(DEFAULT_REQUEST_ATTRIBUTE_NAME);
try {
String enable = System.getProperty("spring.cloud.mse.sentinel.web.http-method-prefix","true");
if(enable != null){
mseHttpMethodSpecify = Boolean.parseBoolean(enable);
}
String enableContextPath = System.getProperty("spring.cloud.ahas.sentinel.web.context-path", "true");
if (enableContextPath != null) {
contextPathSpecify = Boolean.parseBoolean(enableContextPath);
}
} catch (Exception ignore) {
}
}

public UrlCleaner getUrlCleaner() {
return urlCleaner;
}

public SentinelWebMvcConfig setUrlCleaner(UrlCleaner urlCleaner) {
this.urlCleaner = urlCleaner;
return this;
}

public boolean isHttpMethodSpecify() {
return httpMethodSpecify;
}

public SentinelWebMvcConfig setHttpMethodSpecify(boolean httpMethodSpecify) {
this.httpMethodSpecify = httpMethodSpecify;
return this;
}

public static boolean isMseHttpMethodSpecify() {
return mseHttpMethodSpecify;
}

public boolean isWebContextUnify() {
return webContextUnify;
}

public SentinelWebMvcConfig setWebContextUnify(boolean webContextUnify) {
this.webContextUnify = webContextUnify;
return this;
}

public boolean isContextPathSpecify() {
return contextPathSpecify;
}

public SentinelWebMvcConfig setContextPathSpecify(boolean contextPathSpecify) {
this.contextPathSpecify = contextPathSpecify;
return this;
}

@Override
public String toString() {
return "SentinelWebMvcConfig{" +
"urlCleaner=" + urlCleaner +
", httpMethodSpecify=" + httpMethodSpecify +
", webContextUnify=" + webContextUnify +
", contextPathSpecify=" + contextPathSpecify +
", requestAttributeName='" + requestAttributeName + '\'' +
", blockExceptionHandler=" + blockExceptionHandler +
", originParser=" + originParser +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 1999-2024 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
*
* https://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.webmvc_v6x.config;

/**
* @since 1.8.8
*/
public class SentinelWebMvcTotalConfig extends BaseWebMvcConfig {

public static final String DEFAULT_TOTAL_RESOURCE_NAME = "spring-mvc-total-url-request";
public static final String DEFAULT_REQUEST_ATTRIBUTE_NAME = "$$sentinel_spring_web_total_entry_attr";

private String totalResourceName = DEFAULT_TOTAL_RESOURCE_NAME;

public SentinelWebMvcTotalConfig() {
super();
setRequestAttributeName(DEFAULT_REQUEST_ATTRIBUTE_NAME);
}

public String getTotalResourceName() {
return totalResourceName;
}

public SentinelWebMvcTotalConfig setTotalResourceName(String totalResourceName) {
this.totalResourceName = totalResourceName;
return this;
}

@Override
public String toString() {
return "SentinelWebMvcTotalConfig{" +
"totalResourceName='" + totalResourceName + '\'' +
", requestAttributeName='" + requestAttributeName + '\'' +
", blockExceptionHandler=" + blockExceptionHandler +
", originParser=" + originParser +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright 1999-2024 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.webmvc_v6x.config;

import com.alibaba.csp.sentinel.config.SentinelConfig;
import com.alibaba.csp.sentinel.log.RecordLog;
import com.alibaba.csp.sentinel.util.StringUtil;

/**
* The configuration center for Web Servlet adapter (ported to Spring Web adapter).
*
* @since 1.8.8
*/
public final class WebServletLocalConfig {

public static final String BLOCK_PAGE_URL_CONF_KEY = "csp.sentinel.web.servlet.block.page";
public static final String BLOCK_PAGE_HTTP_STATUS_CONF_KEY = "csp.sentinel.web.servlet.block.status";
public static final String BLOCK_PAGE_ALLOW_ORIGINS_CONF_KEY = "csp.sentinel.web.servlet.block.cors-allow-origins";

private static final int HTTP_STATUS_TOO_MANY_REQUESTS = 429;

/**
* Get redirecting page when blocked by Sentinel.
*
* @return the block page URL, maybe null if not configured.
*/
public static String getBlockPage() {
return SentinelConfig.getConfig(BLOCK_PAGE_URL_CONF_KEY);
}

public static void setBlockPage(String blockPage) {
SentinelConfig.setConfig(BLOCK_PAGE_URL_CONF_KEY, blockPage);
}

/**
* <p>Get the HTTP status when using the default block page.</p>
* <p>You can set the status code with the {@code -Dcsp.sentinel.web.servlet.block.status}
* property. When the property is empty or invalid, Sentinel will use 429 (Too Many Requests)
* as the default status code.</p>
*
* @return the HTTP status of the default block page
*/
public static int getBlockPageHttpStatus() {
String value = SentinelConfig.getConfig(BLOCK_PAGE_HTTP_STATUS_CONF_KEY);
if (StringUtil.isEmpty(value)) {
return HTTP_STATUS_TOO_MANY_REQUESTS;
}
try {
int s = Integer.parseInt(value);
if (s <= 0) {
throw new IllegalArgumentException("Invalid status code: " + s);
}
return s;
} catch (Exception e) {
RecordLog.warn("[WebServletConfig] Invalid block HTTP status (" + value + "), using default 429");
setBlockPageHttpStatus(HTTP_STATUS_TOO_MANY_REQUESTS);
}
return HTTP_STATUS_TOO_MANY_REQUESTS;
}

/**
* Set the HTTP status of the default block page.
*
* @param httpStatus the HTTP status of the default block page
*/
public static void setBlockPageHttpStatus(int httpStatus) {
if (httpStatus <= 0) {
throw new IllegalArgumentException("Invalid HTTP status code: " + httpStatus);
}
SentinelConfig.setConfig(BLOCK_PAGE_HTTP_STATUS_CONF_KEY, String.valueOf(httpStatus));
}

private WebServletLocalConfig() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 1999-2024 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
*
* https://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.webmvc_v6x;

import com.alibaba.fastjson.JSONObject;

/**
* @author kaizi2009
*/
public class ResultWrapper {

private Integer code;
private String message;

public ResultWrapper(Integer code, String message) {
this.code = code;
this.message = message;
}

public Integer getCode() {
return code;
}

public void setCode(Integer code) {
this.code = code;
}

public static ResultWrapper error() {

return new ResultWrapper(-1, "System error");
}

public static ResultWrapper blocked() {
return new ResultWrapper(-2, "Blocked by Sentinel");
}

public String toJsonString() {
return JSONObject.toJSONString(this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* Copyright 1999-2024 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
*
* https://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.webmvc_v6x;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.alibaba.csp.sentinel.node.ClusterNode;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot;
import com.alibaba.csp.sentinel.util.StringUtil;

import java.util.Collections;

import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

/**
* @author kaizi2009
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestApplication.class)
@AutoConfigureMockMvc
public class SentinelSpringMvcIntegrationTest {

private static final String HELLO_STR = "Hello!";
@Autowired
private MockMvc mvc;

@Test
public void testBase() throws Exception {
String url = "/hello";
this.mvc.perform(get(url))
.andExpect(status().isOk())
.andExpect(content().string(HELLO_STR));

ClusterNode cn = ClusterBuilderSlot.getClusterNode(url);
assertNotNull(cn);
assertEquals(1, cn.passQps(), 0.01);
}

@Test
public void testOriginParser() throws Exception {
String springMvcPathVariableUrl = "/foo/{id}";
String limitOrigin = "userA";
final String headerName = "S-User";
configureRulesFor(springMvcPathVariableUrl, 0, limitOrigin);

// This will be passed since the caller is different: userB
this.mvc.perform(get("/foo/1").accept(MediaType.TEXT_PLAIN).header(headerName, "userB"))
.andExpect(status().isOk())
.andExpect(content().string("foo 1"));

// This will be blocked since the caller is same: userA
this.mvc.perform(
get("/foo/2").accept(MediaType.APPLICATION_JSON).header(headerName, limitOrigin))
.andExpect(status().isOk())
.andExpect(content().json(ResultWrapper.blocked().toJsonString()));

// This will be passed since the caller is different: ""
this.mvc.perform(get("/foo/3").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().string("foo 3"));

FlowRuleManager.loadRules(null);
}

@Test
public void testTotalInterceptor() throws Exception {
String url = "/hello";
String totalTarget = "my_spring_mvc_total_url_request";
for (int i = 0; i < 3; i++) {
this.mvc.perform(get(url))
.andExpect(status().isOk())
.andExpect(content().string(HELLO_STR));
}
ClusterNode cn = ClusterBuilderSlot.getClusterNode(totalTarget);
assertNotNull(cn);
assertEquals(3, cn.passQps(), 0.01);
}

@Test
public void testRuntimeException() throws Exception {
String url = "/runtimeException";
configureExceptionRulesFor(url, 3, null);
int repeat = 3;
for (int i = 0; i < repeat; i++) {
this.mvc.perform(get(url))
.andExpect(status().isOk())
.andExpect(content().string(ResultWrapper.error().toJsonString()));
ClusterNode cn = ClusterBuilderSlot.getClusterNode(url);
assertNotNull(cn);
assertEquals(i + 1, cn.passQps(), 0.01);
}

// This will be blocked and response json.
this.mvc.perform(get(url))
.andExpect(status().isOk())
.andExpect(content().string(ResultWrapper.blocked().toJsonString()));
ClusterNode cn = ClusterBuilderSlot.getClusterNode(url);
assertNotNull(cn);
assertEquals(repeat, cn.passQps(), 0.01);
assertEquals(1, cn.blockRequest(), 1);
}

private void configureRulesFor(String resource, int count, String limitApp) {
FlowRule rule = new FlowRule()
.setCount(count)
.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setResource(resource);
if (StringUtil.isNotBlank(limitApp)) {
rule.setLimitApp(limitApp);
}
FlowRuleManager.loadRules(Collections.singletonList(rule));
}

private void configureExceptionRulesFor(String resource, int count, String limitApp) {
FlowRule rule = new FlowRule()
.setCount(count)
.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO);
rule.setResource(resource);
if (StringUtil.isNotBlank(limitApp)) {
rule.setLimitApp(limitApp);
}
FlowRuleManager.loadRules(Collections.singletonList(rule));
}

@After
public void cleanUp() {
FlowRuleManager.loadRules(null);
ClusterBuilderSlot.resetClusterNodes();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 1999-2024 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.webmvc_v6x;

import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.config.SentinelWebMvcConfig;

import org.junit.Test;

/**
* @author Eric Zhao
*/
public class SentinelWebInterceptorTest {

@Test(expected = IllegalArgumentException.class)
public void testPassIllegalConfig() {
SentinelWebMvcConfig config = new SentinelWebMvcConfig();
config.setRequestAttributeName(null);
SentinelWebInterceptor interceptor = new SentinelWebInterceptor(config);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 1999-2024 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
*
* https://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.webmvc_v6x;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* @author kaizi2009
*/
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback;

import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;

import static org.junit.Assert.assertEquals;

public class DefaultBlockExceptionHandlerTest {

@Test
public void handle_writeBlockPage() throws Exception {
DefaultBlockExceptionHandler h = new DefaultBlockExceptionHandler();
MockHttpServletRequest req = new MockHttpServletRequest("GET", "/a/b/c");
req.setQueryString("a=1&b=2");
MockHttpServletResponse resp = new MockHttpServletResponse();
String resourceName = "/a/b/c";
BlockException ex = new FlowException("msg");
h.handle(req, resp, resourceName, ex);
assertEquals(429, resp.getStatus());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright 1999-2024 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
*
* https://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.webmvc_v6x.config;

import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.SentinelWebInterceptor;
import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.SentinelWebTotalInterceptor;
import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback.RequestOriginParser;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
* Config sentinel interceptor
*
* @author kaizi2009
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
//Add sentinel interceptor
addSpringMvcInterceptor(registry);

//If you want to sentinel the total flow, you can add total interceptor
addSpringMvcTotalInterceptor(registry);
}

private void addSpringMvcInterceptor(InterceptorRegistry registry) {
//Config
SentinelWebMvcConfig config = new SentinelWebMvcConfig();

config.setBlockExceptionHandler(new BlockExceptionHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, String resourceName, BlockException e) throws Exception {
resourceName = e.getRule().getResource();
//Depending on your situation, you can choose to process or throw
if ("/hello".equals(resourceName)) {
//Do something ......
//Write string or json string;
response.getWriter().write("/Blocked by sentinel");
} else {
//Handle in global exception handling
throw e;
}
}
});

//Custom configuration if necessary
config.setHttpMethodSpecify(false);
config.setWebContextUnify(true);
config.setOriginParser(new RequestOriginParser() {
@Override
public String parseOrigin(HttpServletRequest request) {
return request.getHeader("S-user");
}
});

//Add sentinel interceptor
registry.addInterceptor(new SentinelWebInterceptor(config)).addPathPatterns("/**");
}

private void addSpringMvcTotalInterceptor(InterceptorRegistry registry) {
//Configure
SentinelWebMvcTotalConfig config = new SentinelWebMvcTotalConfig();

//Custom configuration if necessary
config.setRequestAttributeName("my_sentinel_spring_mvc_total_entity_container");
config.setTotalResourceName("my_spring_mvc_total_url_request");

//Add sentinel interceptor
registry.addInterceptor(new SentinelWebTotalInterceptor(config)).addPathPatterns("/**");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 1999-2024 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
*
* https://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.webmvc_v6x.config;

import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.ResultWrapper;
import com.alibaba.csp.sentinel.slots.block.AbstractRule;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
* Config 'BlockException' handler, handler it in spring veb 'ExceptionHandler'
*
* @author kaizi2009
*/
@ControllerAdvice
@Order(0)
public class SentinelSpringMvcBlockHandlerConfig {
private Logger logger = LoggerFactory.getLogger(this.getClass());

@ExceptionHandler(BlockException.class)
@ResponseBody
public ResultWrapper sentinelBlockHandler(BlockException e) {
AbstractRule rule = e.getRule();
//Log
logger.info("Blocked by sentinel, {}", rule.toString());
//Return object
return ResultWrapper.blocked();
}

@ExceptionHandler(Exception.class)
@ResponseBody
public ResultWrapper exceptionHandler(Exception e) {
logger.error("System error", e.getMessage());
return new ResultWrapper(-1, "System error");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 1999-2024 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
*
* https://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.webmvc_v6x.controller;


import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

/**
* @author kaizi2009
*/
@RestController
public class TestController {

@GetMapping("/hello")
public String apiHello() {
return "Hello!";
}

@GetMapping("/err")
public String apiError() {
return "Oops...";
}

@GetMapping("/foo/{id}")
public String apiFoo(@PathVariable("id") Long id) {
return "foo " + id;
}

@GetMapping("/runtimeException")
public String runtimeException() {
int i = 1 / 0;
return "runtimeException";
}

@GetMapping("/exclude/{id}")
public String apiExclude(@PathVariable("id") Long id) {
return "Exclude " + id;
}

}
34 changes: 34 additions & 0 deletions sentinel-adapter/sentinel-web-adapter-common/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?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.8.7</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sentinel-web-adapter-common</artifactId>
<packaging>jar</packaging>
<properties>
<java.source.version>8</java.source.version>
<java.target.version>8</java.target.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 1999-2024 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
*
* https://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.web.common;

/**
* Unify the resource target.
*
* @since 1.8.8
*/
public interface UrlCleaner {

/**
* Unify the resource target.
*
* @param originUrl the original URL
* @return the unified resource name
*/
String clean(String originUrl);
}