diff --git a/sentinel-adapter/pom.xml b/sentinel-adapter/pom.xml index 14bebd62..a8ef8810 100755 --- a/sentinel-adapter/pom.xml +++ b/sentinel-adapter/pom.xml @@ -27,6 +27,7 @@ sentinel-spring-cloud-gateway-adapter sentinel-spring-webmvc-adapter sentinel-zuul2-adapter + sentinel-jax-rs-adapter diff --git a/sentinel-adapter/sentinel-jax-rs-adapter/README.md b/sentinel-adapter/sentinel-jax-rs-adapter/README.md new file mode 100755 index 00000000..ebc35cd4 --- /dev/null +++ b/sentinel-adapter/sentinel-jax-rs-adapter/README.md @@ -0,0 +1,79 @@ +# Sentinel for jax-rs + +Sentinel provides filter integration to enable flow control for web requests. +Add the following dependency in `pom.xml` (if you are using Maven): + +```xml + + com.alibaba.csp + sentinel-jax-rs-adapter + x.y.z + +``` + +## SentinelJaxRsProviderFilter + +the `SentinelJaxRsProviderFilter` is auto activated in pure jax-rs application. + +For Spring web applications you can configure with Spring bean: + +```java +@Configuration +public class FilterConfig { + + @Bean + public SentinelJaxRsProviderFilter sentinelJaxRsProviderFilter() { + return new SentinelJaxRsProviderFilter(); + } +} +``` + +## DefaultExceptionMapper + +Sentinel provides DefaultExceptionMapper to map Throwable to Response(with Status.INTERNAL_SERVER_ERROR), in order to let SentinelJaxRsProviderFilter to be called and exit the sentinel entry. + +according to `3.3.4 Exceptions` of [jaxrs-2_1-final-spec](https://download.oracle.com/otn-pub/jcp/jaxrs-2_1-final-eval-spec/jaxrs-2_1-final-spec.pdf): +>Checked exceptions and throwables that have not been mapped and cannot be thrown directly MUSTbe wrapped in a container-specific exception that is then thrown and allowed to propagate to the un-derlying container. + +if WebApplicationException or its subclasses is thrown, there are automated converted to Response and can enter response filter. + +if throw exception which is not WebApplicationException or its subclass, and not matched by custom exception mapper, then the response filter cannot be called. for this case, I thank a default exception mapper maybe introduced. + +according to `4.4 Exception Mapping Providers` of [jaxrs-2_1-final-spec](https://download.oracle.com/otn-pub/jcp/jaxrs-2_1-final-eval-spec/jaxrs-2_1-final-spec.pdf): +>When choosing an exception mapping provider to map an exception, an implementation MUST use theprovider whose generic type is the nearest superclass of the exception. If two or more exception providers are applicable, the one with the highest priority MUST be chosen as described in Section 4.1.3. + +if user also provide custom exception mapper of `Throwable`, then user has the responsibility to convert it to response and then the response filter can be called. + +as describe in `6.7.1 exceptions` of [jaxrs-2_1-final-spec](https://download.oracle.com/otn-pub/jcp/jaxrs-2_1-final-eval-spec/jaxrs-2_1-final-spec.pdf): +>A response mapped from an exception MUST be processed using the ContainerResponsefilter chain and theWriteTointerceptor chain (if an entity is present in the mapped response). + +## SentinelJaxRsClientTemplate + +For jax-rs client, we provide `SentinelJaxRsClientTemplate` you can use it like this: + +``` + Response response = SentinelJaxRsClientTemplate.execute(resourceName, new Supplier() { + @Override + public Response get() { + return client.target(host).path(url).request() + .get(); + } + }); +``` + +or executeAsync like this: + +``` + Future future = SentinelJaxRsClientTemplate.executeAsync(resourceName, new Supplier>() { + @Override + public Future get() { + return client.target(host).path(url).request() + .async() + .get(); + } + }); +``` + +When a request is blocked, Sentinel jax-rs filter will return Response with status of TOO_MANY_REQUESTS indicating the request is rejected. + +You can customize it by implement your own `SentinelJaxRsFallback` and register to `SentinelJaxRsConfig`. diff --git a/sentinel-adapter/sentinel-jax-rs-adapter/pom.xml b/sentinel-adapter/sentinel-jax-rs-adapter/pom.xml new file mode 100755 index 00000000..b0a94298 --- /dev/null +++ b/sentinel-adapter/sentinel-jax-rs-adapter/pom.xml @@ -0,0 +1,74 @@ + + + 4.0.0 + + + com.alibaba.csp + sentinel-adapter + 1.8.0-SNAPSHOT + + + sentinel-jax-rs-adapter + jar + + + 2.1.1 + + + + + com.alibaba.csp + sentinel-core + + + + javax.ws.rs + javax.ws.rs-api + ${javax.ws.rs-api.version} + provided + + + + junit + junit + test + + + org.springframework.boot + spring-boot-starter-web + 2.2.6.RELEASE + test + + + org.springframework.boot + spring-boot-starter-test + 2.2.6.RELEASE + test + + + io.rest-assured + rest-assured + 4.3.0 + test + + + org.jboss.resteasy + resteasy-spring-boot-starter + 4.5.1.Final + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + always + + + + + diff --git a/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/SentinelJaxRsClientTemplate.java b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/SentinelJaxRsClientTemplate.java new file mode 100644 index 00000000..07abea3d --- /dev/null +++ b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/SentinelJaxRsClientTemplate.java @@ -0,0 +1,79 @@ +/* + * Copyright 1999-2020 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.jaxrs; + +import com.alibaba.csp.sentinel.*; +import com.alibaba.csp.sentinel.adapter.jaxrs.config.SentinelJaxRsConfig; +import com.alibaba.csp.sentinel.adapter.jaxrs.future.FutureWrapper; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.util.function.Supplier; + +import javax.ws.rs.core.Response; +import java.util.concurrent.Future; + + +/** + * wrap jax-rs client execution with sentinel + *
+ *         Response response = SentinelJaxRsClientTemplate.execute(resourceName, new Supplier() {
+ *
+ *             @Override
+ *             public Response get() {
+ *                 return client.target(host).path(url).request()
+ *                         .get();
+ *             }
+ *         });
+ * 
+ * @author sea + */ +public class SentinelJaxRsClientTemplate { + + /** + * execute supplier with sentinel + * @param resourceName + * @param supplier + * @return + */ + public static Response execute(String resourceName, Supplier supplier) { + Entry entry = null; + try { + entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.OUT); + return supplier.get(); + } catch (BlockException ex) { + return SentinelJaxRsConfig.getJaxRsFallback().fallbackResponse(resourceName, ex); + } finally { + System.out.println("entry exit"); + if (entry != null) { + entry.exit(); + } + } + } + + /** + * execute supplier with sentinel + * @param resourceName + * @param supplier + * @return + */ + public static Future executeAsync(String resourceName, Supplier> supplier) { + try { + AsyncEntry entry = SphU.asyncEntry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.OUT); + return new FutureWrapper<>(entry, supplier.get()); + } catch (BlockException ex) { + return SentinelJaxRsConfig.getJaxRsFallback().fallbackFutureResponse(resourceName, ex); + } + } +} diff --git a/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/SentinelJaxRsProviderFilter.java b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/SentinelJaxRsProviderFilter.java new file mode 100644 index 00000000..ffe4edb2 --- /dev/null +++ b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/SentinelJaxRsProviderFilter.java @@ -0,0 +1,88 @@ +/* + * Copyright 1999-2020 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.jaxrs; + +import com.alibaba.csp.sentinel.*; +import com.alibaba.csp.sentinel.adapter.jaxrs.config.SentinelJaxRsConfig; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.util.StringUtil; + +import javax.ws.rs.container.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.ext.Provider; +import java.io.IOException; + +/** + * @author sea + */ +@Provider +public class SentinelJaxRsProviderFilter implements ContainerRequestFilter, ContainerResponseFilter { + + private static final String SENTINEL_JAX_RS_PROVIDER_CONTEXT_NAME = "sentinel_jax_rs_provider_context"; + + + private static final String SENTINEL_JAX_RS_PROVIDER_ENTRY_PROPERTY = "sentinel_jax_rs_provider_entry_property"; + + @Context + private ResourceInfo resourceInfo; + + @Override + public void filter(ContainerRequestContext containerRequestContext) throws IOException { + + try { + String resourceName = getResourceName(containerRequestContext, resourceInfo); + + if (StringUtil.isNotEmpty(resourceName)) { + // Parse the request origin using registered origin parser. + String origin = parseOrigin(containerRequestContext); + String contextName = getContextName(containerRequestContext); + ContextUtil.enter(contextName, origin); + Entry entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.IN); + + containerRequestContext.setProperty(SENTINEL_JAX_RS_PROVIDER_ENTRY_PROPERTY, entry); + } + } catch (BlockException e) { + try { + containerRequestContext.abortWith(SentinelJaxRsConfig.getJaxRsFallback().fallbackResponse(containerRequestContext.getUriInfo().getPath(), e)); + } finally { + ContextUtil.exit(); + } + } + } + + @Override + public void filter(ContainerRequestContext containerRequestContext, ContainerResponseContext containerResponseContext) throws IOException { + Entry entry = (Entry) containerRequestContext.getProperty(SENTINEL_JAX_RS_PROVIDER_ENTRY_PROPERTY); + if (entry != null) { + entry.exit(); + } + containerRequestContext.removeProperty(SENTINEL_JAX_RS_PROVIDER_ENTRY_PROPERTY); + ContextUtil.exit(); + } + + public String getResourceName(ContainerRequestContext containerRequestContext, ResourceInfo resourceInfo) { + return SentinelJaxRsConfig.getResourceNameParser().parse(containerRequestContext, resourceInfo); + } + + protected String getContextName(ContainerRequestContext request) { + return SENTINEL_JAX_RS_PROVIDER_CONTEXT_NAME; + } + + protected String parseOrigin(ContainerRequestContext request) { + return SentinelJaxRsConfig.getRequestOriginParser().parseOrigin(request); + } +} diff --git a/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/config/SentinelJaxRsConfig.java b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/config/SentinelJaxRsConfig.java new file mode 100644 index 00000000..a2684ed2 --- /dev/null +++ b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/config/SentinelJaxRsConfig.java @@ -0,0 +1,62 @@ +/* + * Copyright 1999-2020 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.jaxrs.config; + +import com.alibaba.csp.sentinel.adapter.jaxrs.fallback.DefaultSentinelJaxRsFallback; +import com.alibaba.csp.sentinel.adapter.jaxrs.fallback.SentinelJaxRsFallback; +import com.alibaba.csp.sentinel.adapter.jaxrs.request.DefaultRequestOriginParser; +import com.alibaba.csp.sentinel.adapter.jaxrs.request.DefaultResourceNameParser; +import com.alibaba.csp.sentinel.adapter.jaxrs.request.RequestOriginParser; +import com.alibaba.csp.sentinel.adapter.jaxrs.request.ResourceNameParser; + +/** + * @author sea + */ +public class SentinelJaxRsConfig { + + private static volatile ResourceNameParser resourceNameParser = new DefaultResourceNameParser(); + + private static volatile RequestOriginParser requestOriginParser = new DefaultRequestOriginParser(); + + private static volatile SentinelJaxRsFallback jaxRsFallback = new DefaultSentinelJaxRsFallback(); + + public static ResourceNameParser getResourceNameParser() { + return resourceNameParser; + } + + public static void setResourceNameParser(ResourceNameParser resourceNameParser) { + SentinelJaxRsConfig.resourceNameParser = resourceNameParser; + } + + public static RequestOriginParser getRequestOriginParser() { + return requestOriginParser; + } + + public static void setRequestOriginParser(RequestOriginParser originParser) { + SentinelJaxRsConfig.requestOriginParser = originParser; + } + + public static SentinelJaxRsFallback getJaxRsFallback() { + return jaxRsFallback; + } + + public static void setJaxRsFallback(SentinelJaxRsFallback jaxRsFallback) { + SentinelJaxRsConfig.jaxRsFallback = jaxRsFallback; + } + + private SentinelJaxRsConfig() { + } +} diff --git a/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/exception/DefaultExceptionMapper.java b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/exception/DefaultExceptionMapper.java new file mode 100644 index 00000000..a441204a --- /dev/null +++ b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/exception/DefaultExceptionMapper.java @@ -0,0 +1,39 @@ +/* + * Copyright 1999-2020 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.jaxrs.exception; + + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +/** + * sentinel jax-rs adapter provide this exception mapper + * in case of user throw exception which is not {@link javax.ws.rs.WebApplicationException} and not matched by any ExceptionMapper + * this exception mapper convert exception to Response let ContainerResponseFilter to be called to exit sentinel entry + * user can add custom ExceptionMapper and config with {@link javax.annotation.Priority} with lower value + * @author sea + */ +@Provider +public class DefaultExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(Throwable exception) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(exception.getMessage()) + .build(); + } +} diff --git a/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/fallback/DefaultSentinelJaxRsFallback.java b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/fallback/DefaultSentinelJaxRsFallback.java new file mode 100644 index 00000000..d1f93772 --- /dev/null +++ b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/fallback/DefaultSentinelJaxRsFallback.java @@ -0,0 +1,45 @@ +/* + * Copyright 1999-2020 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.jaxrs.fallback; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; + +/** + * @author sea + */ +public class DefaultSentinelJaxRsFallback implements SentinelJaxRsFallback { + @Override + public Response fallbackResponse(String route, Throwable cause) { + return Response.status(Response.Status.TOO_MANY_REQUESTS) + .entity("Blocked by Sentinel (flow limiting)") + .type(MediaType.APPLICATION_JSON_TYPE) + .build(); + } + + @Override + public Future fallbackFutureResponse(final String route, final Throwable cause) { + return new FutureTask<>(new Callable() { + @Override + public Response call() throws Exception { + return fallbackResponse(route, cause); + } + }); + } +} diff --git a/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/fallback/SentinelJaxRsFallback.java b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/fallback/SentinelJaxRsFallback.java new file mode 100644 index 00000000..b5cf6115 --- /dev/null +++ b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/fallback/SentinelJaxRsFallback.java @@ -0,0 +1,43 @@ +/* + * Copyright 1999-2020 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.jaxrs.fallback; + +import javax.ws.rs.core.Response; +import java.util.concurrent.Future; + +/** + * @author sea + */ +public interface SentinelJaxRsFallback { + + /** + * Provides a fallback response based on the cause of the failed execution. + * + * @param route The route the fallback is for + * @param cause cause of the main method failure, may be null + * @return the fallback response + */ + Response fallbackResponse(String route, Throwable cause); + + /** + * Provides a fallback response future based on the cause of the failed execution. + * + * @param route The route the fallback is for + * @param cause cause of the main method failure, may be null + * @return the fallback response future + */ + Future fallbackFutureResponse(String route, Throwable cause); +} diff --git a/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/future/FutureWrapper.java b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/future/FutureWrapper.java new file mode 100644 index 00000000..bc332114 --- /dev/null +++ b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/future/FutureWrapper.java @@ -0,0 +1,81 @@ +/* + * Copyright 1999-2020 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.jaxrs.future; + +import com.alibaba.csp.sentinel.AsyncEntry; + +import java.util.concurrent.*; + +/** + * wrap Future to ensure entry exit + * @author sea + */ +public class FutureWrapper implements Future { + + AsyncEntry entry; + + Future future; + + public FutureWrapper(AsyncEntry entry, Future future) { + this.entry = entry; + this.future = future; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + try { + return future.cancel(mayInterruptIfRunning); + } finally { + exitEntry(); + } + + } + + @Override + public boolean isCancelled() { + return future.isCancelled(); + } + + @Override + public boolean isDone() { + return future.isDone(); + } + + @Override + public V get() throws InterruptedException, ExecutionException { + try { + return future.get(); + } finally { + exitEntry(); + } + } + + @Override + public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + try { + return future.get(timeout, unit); + } finally { + exitEntry(); + } + } + + private void exitEntry() { + if (entry != null) { + entry.exit(); + } + } + +} diff --git a/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/request/DefaultRequestOriginParser.java b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/request/DefaultRequestOriginParser.java new file mode 100644 index 00000000..b8ab0f9c --- /dev/null +++ b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/request/DefaultRequestOriginParser.java @@ -0,0 +1,31 @@ +/* + * Copyright 1999-2020 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.jaxrs.request; + +import javax.ws.rs.container.ContainerRequestContext; + +/** + * @author sea + */ +public class DefaultRequestOriginParser implements RequestOriginParser { + + private static final String EMPTY_ORIGIN = ""; + + @Override + public String parseOrigin(ContainerRequestContext request) { + return EMPTY_ORIGIN; + } +} diff --git a/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/request/DefaultResourceNameParser.java b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/request/DefaultResourceNameParser.java new file mode 100644 index 00000000..efc8c7ed --- /dev/null +++ b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/request/DefaultResourceNameParser.java @@ -0,0 +1,33 @@ +/* + * Copyright 1999-2020 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.jaxrs.request; + +import javax.ws.rs.Path; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ResourceInfo; + +/** + * @author sea + */ +public class DefaultResourceNameParser implements ResourceNameParser { + @Override + public String parse(ContainerRequestContext containerRequestContext, ResourceInfo resourceInfo) { + Path classPath = resourceInfo.getResourceClass().getAnnotation(Path.class); + Path methodPath = resourceInfo.getResourceMethod().getAnnotation(Path.class); + return containerRequestContext.getRequest().getMethod() + ":" + (classPath != null ? classPath.value() : "") + (methodPath != null ? methodPath.value() : ""); + } + +} diff --git a/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/request/RequestOriginParser.java b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/request/RequestOriginParser.java new file mode 100644 index 00000000..0eff671f --- /dev/null +++ b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/request/RequestOriginParser.java @@ -0,0 +1,34 @@ +/* + * Copyright 1999-2020 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.jaxrs.request; + +import javax.ws.rs.container.ContainerRequestContext; + +/** + * The origin parser parses request origin (e.g. IP, user, appName) from HTTP request. + * + * @author sea + */ +public interface RequestOriginParser { + + /** + * Parse the origin from given HTTP request. + * + * @param request HTTP request + * @return parsed origin + */ + String parseOrigin(ContainerRequestContext request); +} diff --git a/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/request/ResourceNameParser.java b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/request/ResourceNameParser.java new file mode 100644 index 00000000..096c31bf --- /dev/null +++ b/sentinel-adapter/sentinel-jax-rs-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/jaxrs/request/ResourceNameParser.java @@ -0,0 +1,27 @@ +/* + * Copyright 1999-2020 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.jaxrs.request; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ResourceInfo; + +/** + * @author sea + */ +public interface ResourceNameParser { + + String parse(ContainerRequestContext containerRequestContext, ResourceInfo resourceInfo); +} diff --git a/sentinel-adapter/sentinel-jax-rs-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/jaxrs/ClientFilterTest.java b/sentinel-adapter/sentinel-jax-rs-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/jaxrs/ClientFilterTest.java new file mode 100644 index 00000000..91c01e11 --- /dev/null +++ b/sentinel-adapter/sentinel-jax-rs-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/jaxrs/ClientFilterTest.java @@ -0,0 +1,413 @@ +/* + * Copyright 1999-2020 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.jaxrs; + +import com.alibaba.csp.sentinel.Constants; +import com.alibaba.csp.sentinel.CtSph; +import com.alibaba.csp.sentinel.adapter.jaxrs.config.SentinelJaxRsConfig; +import com.alibaba.csp.sentinel.adapter.jaxrs.fallback.SentinelJaxRsFallback; +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.node.EntranceNode; +import com.alibaba.csp.sentinel.node.Node; +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 com.alibaba.csp.sentinel.util.function.Supplier; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.util.SocketUtils; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.concurrent.*; + +import static org.junit.Assert.*; +import static org.junit.Assert.assertNull; + +/** + * @author sea + */ +public class ClientFilterTest { + + private static final String HELLO_STR = "Hello!"; + + static int port; + + static String host; + + static Client client; + + static ConfigurableApplicationContext ctx; + + @BeforeClass + public static void startApplication() { + client = ClientBuilder.newBuilder() + .connectTimeout(3, TimeUnit.SECONDS) + .readTimeout(3, TimeUnit.SECONDS) + .build(); + + port = SocketUtils.findAvailableTcpPort(); + host = "http://127.0.0.1:" + port; + SpringApplication springApplication = new SpringApplication(TestApplication.class); + ctx = springApplication.run("--spring.profiles.active=client", "--server.port=" + port); + } + + @AfterClass + public static void shutdown() { + ctx.close(); + + Context context = ContextUtil.getContext(); + if (context != null) { + context.setCurEntry(null); + ContextUtil.exit(); + } + + Constants.ROOT.removeChildList(); + + ClusterBuilderSlot.getClusterNodeMap().clear(); + + // Clear chainMap in CtSph + try { + Method resetChainMapMethod = CtSph.class.getDeclaredMethod("resetChainMap"); + resetChainMapMethod.setAccessible(true); + resetChainMapMethod.invoke(null); + } catch (Exception e) { + // Empty + } + } + + @After + public void cleanUp() { + FlowRuleManager.loadRules(null); + ClusterBuilderSlot.resetClusterNodes(); + } + + @Test + public void testClientGetHello() { + final String url = "/test/hello"; + String resourceName = "GET:" + url; + Response response = SentinelJaxRsClientTemplate.execute(resourceName, new Supplier() { + + @Override + public Response get() { + return client.target(host).path(url).request() + .get(); + } + }); + assertEquals(200, response.getStatus()); + assertEquals(HELLO_STR, response.readEntity(String.class)); + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName); + assertNotNull(cn); + assertEquals(1, cn.passQps(), 0.01); + + String context = ""; + for (Node n : Constants.ROOT.getChildList()) { + if (n instanceof EntranceNode) { + String id = ((EntranceNode) n).getId().getName(); + if (url.equals(id)) { + context = ((EntranceNode) n).getId().getName(); + } + } + } + assertEquals("", context); + } + + @Test + public void testClientAsyncGetHello() throws InterruptedException, ExecutionException { + final String url = "/test/async-hello"; + final String resourceName = "GET:" + url; + + Future future = SentinelJaxRsClientTemplate.executeAsync(resourceName, new Supplier>() { + @Override + public Future get() { + return client.target(host).path(url).request() + .async() + .get(); + } + }); + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName); + assertNotNull(cn); + assertEquals(HELLO_STR, future.get().readEntity(String.class)); + + cn = ClusterBuilderSlot.getClusterNode(resourceName); + assertNotNull(cn); + assertEquals(1, cn.passQps(), 0.01); + } + + @Test + public void testCustomResourceName() { + final String url = "/test/hello/{name}"; + final String resourceName = "GET:" + url; + + Response response1 = SentinelJaxRsClientTemplate.execute(resourceName, new Supplier() { + @Override + public Response get() { + return client.target(host) + .path(url) + .resolveTemplate("name", "abc") + .request() + .get(); + } + }); + assertEquals(200, response1.getStatus()); + assertEquals("Hello abc !", response1.readEntity(String.class)); + + Response response2 = SentinelJaxRsClientTemplate.execute(resourceName, new Supplier() { + @Override + public Response get() { + return client.target(host) + .path(url) + .resolveTemplate("name", "def") + .request() + .get(); + } + }); + assertEquals(javax.ws.rs.core.Response.Status.OK.getStatusCode(), response2.getStatus()); + assertEquals("Hello def !", response2.readEntity(String.class)); + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName); + assertNotNull(cn); + assertEquals(2, cn.passQps(), 0.01); + + assertNull(ClusterBuilderSlot.getClusterNode("/test/hello/abc")); + assertNull(ClusterBuilderSlot.getClusterNode("/test/hello/def")); + } + + @Test + public void testClientFallback() { + final String url = "/test/hello"; + final String resourceName = "GET:" + url; + configureRulesFor(resourceName, 0); + + Response response = SentinelJaxRsClientTemplate.execute(resourceName, new Supplier() { + @Override + public Response get() { + return client.target(host).path(url).request() + .get(); + } + }); + assertEquals(javax.ws.rs.core.Response.Status.TOO_MANY_REQUESTS.getStatusCode(), response.getStatus()); + assertEquals("Blocked by Sentinel (flow limiting)", response.readEntity(String.class)); + + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName); + assertNotNull(cn); + assertEquals(0, cn.passQps(), 0.01); + } + + @Test + public void testClientCustomFallback() { + final String url = "/test/hello"; + final String resourceName = "GET:" + url; + configureRulesFor(resourceName, 0); + + SentinelJaxRsConfig.setJaxRsFallback(new SentinelJaxRsFallback() { + @Override + public javax.ws.rs.core.Response fallbackResponse(String route, Throwable cause) { + return javax.ws.rs.core.Response.status(javax.ws.rs.core.Response.Status.OK) + .entity("Blocked by Sentinel (flow limiting)") + .type(MediaType.APPLICATION_JSON_TYPE) + .build(); + } + + @Override + public Future fallbackFutureResponse(final String route, final Throwable cause) { + return new FutureTask<>(new Callable() { + @Override + public Response call() throws Exception { + return fallbackResponse(route, cause); + } + }); + } + }); + + Response response = SentinelJaxRsClientTemplate.execute(resourceName, new Supplier() { + @Override + public Response get() { + return client.target(host).path(url).request() + .get(); + } + }); + assertEquals(javax.ws.rs.core.Response.Status.OK.getStatusCode(), response.getStatus()); + assertEquals("Blocked by Sentinel (flow limiting)", response.readEntity(String.class)); + + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName); + assertNotNull(cn); + assertEquals(0, cn.passQps(), 0.01); + } + + @Test + public void testServerReturn400() { + final String url = "/test/400"; + final String resourceName = "GET:" + url; + Response response = SentinelJaxRsClientTemplate.execute(resourceName, new Supplier() { + @Override + public Response get() { + return client.target(host).path(url).request() + .get(); + } + }); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("test return 400", response.readEntity(String.class)); + + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName); + assertNotNull(cn); + assertEquals(1, cn.passQps(), 0.01); + } + + @Test + public void testServerReturn500() { + final String url = "/test/ex"; + final String resourceName = "GET:" + url; + Response response = SentinelJaxRsClientTemplate.execute(resourceName, new Supplier() { + @Override + public Response get() { + return client.target(host).path(url).request() + .get(); + } + }); + assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); + assertEquals("test exception mapper", response.readEntity(String.class)); + + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName); + assertNotNull(cn); + assertEquals(1, cn.passQps(), 0.01); + } + + @Test + public void testServerTimeout() { + final String url = "/test/delay/10"; + final String resourceName = "GET:/test/delay/{seconds}"; + try { + SentinelJaxRsClientTemplate.execute(resourceName, new Supplier() { + @Override + public Response get() { + return client.target(host).path(url).request() + .get(); + } + }); + } catch (ProcessingException e) { + //ignore + } + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName); + assertNotNull(cn); + assertEquals(0, cn.passQps(), 0.01); + } + + @Test + public void testFutureGetServerTimeout() { + final String url = "/test/delay/10"; + final String resourceName = "GET:/test/delay/{seconds}"; + try { + Future future = SentinelJaxRsClientTemplate.executeAsync(resourceName, new Supplier>() { + @Override + public Future get() { + return client.target(host).path(url).request() + .async() + .get(); + } + }); + future.get(); + } catch (Exception e) { + //ignore + } + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName); + assertNotNull(cn); + assertEquals(0, cn.passQps(), 0.01); + } + + @Test + public void testFutureGetTimeout() { + final String url = "/test/delay/10"; + final String resourceName = "GET:/test/delay/{seconds}"; + try { + Future future = SentinelJaxRsClientTemplate.executeAsync(resourceName, new Supplier>() { + @Override + public Future get() { + return client.target(host).path(url).request() + .async() + .get(); + } + }); + future.get(1, TimeUnit.SECONDS); + } catch (Exception e) { + //ignore + } + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName); + assertNotNull(cn); + assertEquals(0, cn.passQps(), 0.01); + } + + @Test + public void testCancelFuture() { + final String url = "/test/delay/10"; + final String resourceName = "GET:/test/delay/{seconds}"; + try { + Future future = SentinelJaxRsClientTemplate.executeAsync(resourceName, new Supplier>() { + @Override + public Future get() { + return client.target(host).path(url).request() + .async() + .get(); + } + }); + future.cancel(false); + } catch (Exception e) { + //ignore + } + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName); + assertNotNull(cn); + assertEquals(1, cn.passQps(), 0.01); + } + + private void configureRulesFor(String resource, int count) { + configureRulesFor(resource, count, "default"); + } + + 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)); + } +} diff --git a/sentinel-adapter/sentinel-jax-rs-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/jaxrs/ProviderFilterTest.java b/sentinel-adapter/sentinel-jax-rs-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/jaxrs/ProviderFilterTest.java new file mode 100644 index 00000000..30614404 --- /dev/null +++ b/sentinel-adapter/sentinel-jax-rs-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/jaxrs/ProviderFilterTest.java @@ -0,0 +1,257 @@ +/* + * Copyright 1999-2020 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.jaxrs; + +import java.util.Collections; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; + +import com.alibaba.csp.sentinel.Constants; +import com.alibaba.csp.sentinel.adapter.jaxrs.config.SentinelJaxRsConfig; +import com.alibaba.csp.sentinel.adapter.jaxrs.fallback.SentinelJaxRsFallback; +import com.alibaba.csp.sentinel.adapter.jaxrs.request.RequestOriginParser; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.node.EntranceNode; +import com.alibaba.csp.sentinel.node.Node; +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 io.restassured.RestAssured; +import io.restassured.response.Response; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.util.SocketUtils; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.MediaType; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.*; + +/** + * @author sea + */ +public class ProviderFilterTest { + + private static final String HELLO_STR = "Hello!"; + + static ConfigurableApplicationContext ctx; + + @BeforeClass + public static void startApplication() { + RestAssured.basePath = ""; + int port = SocketUtils.findAvailableTcpPort(); + RestAssured.port = port; + SpringApplication springApplication = new SpringApplication(TestApplication.class); + ctx = springApplication.run("--spring.profiles.active=provider", "--server.port=" + port); + } + + @AfterClass + public static void shutdown() { + ctx.close(); + } + + @After + public void cleanUp() { + FlowRuleManager.loadRules(null); + ClusterBuilderSlot.resetClusterNodes(); + } + + + @Test + public void testGetHello() { + String url = "/test/hello"; + String resourceName = "GET:" + url; + Response response = given().get(url); + response.then().statusCode(200).body(equalTo(HELLO_STR)); + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName); + assertNotNull(cn); + assertEquals(1, cn.passQps(), 0.01); + + String context = ""; + for (Node n : Constants.ROOT.getChildList()) { + if (n instanceof EntranceNode) { + String id = ((EntranceNode) n).getId().getName(); + if (url.equals(id)) { + context = ((EntranceNode) n).getId().getName(); + } + } + } + assertEquals("", context); + } + + @Test + public void testAsyncGetHello() { + String url = "/test/async-hello"; + String resourceName = "GET:" + url; + Response response = given().get(url); + response.then().statusCode(200).body(equalTo(HELLO_STR)); + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName); + assertNotNull(cn); + assertEquals(1, cn.passQps(), 0.01); + + String context = ""; + for (Node n : Constants.ROOT.getChildList()) { + if (n instanceof EntranceNode) { + String id = ((EntranceNode) n).getId().getName(); + if (url.equals(id)) { + context = ((EntranceNode) n).getId().getName(); + } + } + } + assertEquals("", context); + } + + @Test + public void testUrlPathParam() { + String url = "/test/hello/{name}"; + String resourceName = "GET:" + url; + + String url1 = "/test/hello/abc"; + Response response1 = given().get(url1); + response1.then().statusCode(200).body(equalTo("Hello abc !")); + + String url2 = "/test/hello/def"; + Response response2 = given().get(url2); + response2.then().statusCode(200).body(equalTo("Hello def !")); + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName); + assertNotNull(cn); + assertEquals(2, cn.passQps(), 0.01); + + assertNull(ClusterBuilderSlot.getClusterNode("GET:" + url1)); + assertNull(ClusterBuilderSlot.getClusterNode("GET:" + url2)); + } + + @Test + public void testDefaultFallback() { + String url = "/test/hello"; + String resourceName = "GET:" + url; + configureRulesFor(resourceName, 0); + Response response = given().get(url); + response.then().statusCode(javax.ws.rs.core.Response.Status.TOO_MANY_REQUESTS.getStatusCode()) + .body(equalTo("Blocked by Sentinel (flow limiting)")); + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName); + assertNotNull(cn); + assertEquals(0, cn.passQps(), 0.01); + } + + @Test + public void testCustomFallback() { + String url = "/test/hello"; + String resourceName = "GET:" + url; + SentinelJaxRsConfig.setJaxRsFallback(new SentinelJaxRsFallback() { + @Override + public javax.ws.rs.core.Response fallbackResponse(String route, Throwable cause) { + return javax.ws.rs.core.Response.status(javax.ws.rs.core.Response.Status.OK) + .entity("Blocked by Sentinel (flow limiting)") + .type(MediaType.APPLICATION_JSON_TYPE) + .build(); + } + + @Override + public Future fallbackFutureResponse(final String route, final Throwable cause) { + return new FutureTask<>(new Callable() { + @Override + public javax.ws.rs.core.Response call() throws Exception { + return fallbackResponse(route, cause); + } + }); + } + }); + + + configureRulesFor(resourceName, 0); + Response response = given().get(url); + response.then().statusCode(javax.ws.rs.core.Response.Status.OK.getStatusCode()) + .body(equalTo("Blocked by Sentinel (flow limiting)")); + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName); + assertNotNull(cn); + assertEquals(0, cn.passQps(), 0.01); + } + + @Test + public void testCustomRequestOriginParser() { + String url = "/test/hello"; + String resourceName = "GET:" + url; + + String limitOrigin = "appB"; + final String headerName = "X-APP"; + configureRulesFor(resourceName, 0, limitOrigin); + + SentinelJaxRsConfig.setRequestOriginParser(new RequestOriginParser() { + @Override + public String parseOrigin(ContainerRequestContext request) { + String origin = request.getHeaderString(headerName); + return origin != null ? origin : ""; + } + }); + + Response response = given() + .header(headerName, "appA").get(url); + response.then().statusCode(200).body(equalTo(HELLO_STR)); + + Response blockedResp = given() + .header(headerName, "appB") + .get(url); + blockedResp.then().statusCode(javax.ws.rs.core.Response.Status.TOO_MANY_REQUESTS.getStatusCode()) + .body(equalTo("Blocked by Sentinel (flow limiting)")); + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName); + assertNotNull(cn); + assertEquals(1, cn.passQps(), 0.01); + assertEquals(1, cn.blockQps(), 0.01); + } + + @Test + public void testExceptionMapper() { + String url = "/test/ex"; + String resourceName = "GET:" + url; + Response response = given().get(url); + response.then().statusCode(javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()).body(equalTo("test exception mapper")); + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(resourceName); + assertNotNull(cn); + } + + private void configureRulesFor(String resource, int count) { + configureRulesFor(resource, count, "default"); + } + + 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)); + } +} diff --git a/sentinel-adapter/sentinel-jax-rs-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/jaxrs/TestApplication.java b/sentinel-adapter/sentinel-jax-rs-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/jaxrs/TestApplication.java new file mode 100644 index 00000000..6ffafb95 --- /dev/null +++ b/sentinel-adapter/sentinel-jax-rs-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/jaxrs/TestApplication.java @@ -0,0 +1,30 @@ +/* + * Copyright 1999-2020 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.jaxrs; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author sea + */ +@SpringBootApplication +public class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } +} diff --git a/sentinel-adapter/sentinel-jax-rs-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/jaxrs/TestResource.java b/sentinel-adapter/sentinel-jax-rs-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/jaxrs/TestResource.java new file mode 100644 index 00000000..f5d8bf18 --- /dev/null +++ b/sentinel-adapter/sentinel-jax-rs-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/jaxrs/TestResource.java @@ -0,0 +1,92 @@ +/* + * Copyright 1999-2020 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.jaxrs; + +import org.springframework.stereotype.Component; + +import javax.ws.rs.*; +import javax.ws.rs.container.AsyncResponse; +import javax.ws.rs.container.Suspended; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * @author sea + */ +@Path("/test") +@Component +public class TestResource { + + ExecutorService executor = Executors.newFixedThreadPool(5); + + @Path("/hello") + @GET + @Produces({ MediaType.APPLICATION_JSON }) + public String sayHello() { + return "Hello!"; + } + + @Path("/async-hello") + @GET + @Produces({ MediaType.APPLICATION_JSON }) + public void asyncSayHello(@Suspended final AsyncResponse asyncResponse) { + executor.submit(new Runnable() { + @Override + public void run() { + try { + TimeUnit.MILLISECONDS.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + asyncResponse.resume("Hello!"); + } + }); + } + + @Path("/hello/{name}") + @GET + @Produces({ MediaType.APPLICATION_JSON }) + public String sayHelloWithName(@PathParam(value = "name") String name) { + return "Hello " + name + " !"; + } + + @Path("/ex") + @GET + @Produces({ MediaType.APPLICATION_JSON }) + public String exception() { + throw new RuntimeException("test exception mapper"); + } + + @Path("/400") + @GET + @Produces({ MediaType.APPLICATION_JSON }) + public String badRequest() { + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST) + .entity("test return 400") + .build()); + } + + @Path("/delay/{seconds}") + @GET + @Produces({ MediaType.APPLICATION_JSON }) + public String delay(@PathParam(value = "seconds") long seconds) throws InterruptedException { + TimeUnit.SECONDS.sleep(seconds); + return "finish"; + } +} diff --git a/sentinel-adapter/sentinel-jax-rs-adapter/src/test/resources/application-client.yml b/sentinel-adapter/sentinel-jax-rs-adapter/src/test/resources/application-client.yml new file mode 100644 index 00000000..af8e43ed --- /dev/null +++ b/sentinel-adapter/sentinel-jax-rs-adapter/src/test/resources/application-client.yml @@ -0,0 +1,3 @@ +resteasy: + jaxrs: + scan-packages: com.alibaba.csp.sentinel.adapter.jaxrs.exception diff --git a/sentinel-adapter/sentinel-jax-rs-adapter/src/test/resources/application-provider.yml b/sentinel-adapter/sentinel-jax-rs-adapter/src/test/resources/application-provider.yml new file mode 100644 index 00000000..dc47def2 --- /dev/null +++ b/sentinel-adapter/sentinel-jax-rs-adapter/src/test/resources/application-provider.yml @@ -0,0 +1,3 @@ +resteasy: + jaxrs: + scan-packages: com.alibaba.csp.sentinel.adapter.jaxrs diff --git a/sentinel-demo/pom.xml b/sentinel-demo/pom.xml index 04c07ed1..a5465da0 100755 --- a/sentinel-demo/pom.xml +++ b/sentinel-demo/pom.xml @@ -39,6 +39,7 @@ sentinel-demo-spring-webmvc sentinel-demo-zuul2-gateway sentinel-demo-log-logback + sentinel-demo-jax-rs diff --git a/sentinel-demo/sentinel-demo-jax-rs/pom.xml b/sentinel-demo/sentinel-demo-jax-rs/pom.xml new file mode 100644 index 00000000..d2dc06b3 --- /dev/null +++ b/sentinel-demo/sentinel-demo-jax-rs/pom.xml @@ -0,0 +1,49 @@ + + + + sentinel-demo + com.alibaba.csp + 1.8.0-SNAPSHOT + + 4.0.0 + + sentinel-demo-jax-rs + + + 2.2.6.RELEASE + + + + + com.alibaba.csp + sentinel-core + + + com.alibaba.csp + sentinel-transport-simple-http + + + com.alibaba.csp + sentinel-jax-rs-adapter + ${project.version} + + + org.jboss.resteasy + resteasy-spring-boot-starter + 4.5.1.Final + + + org.springframework.boot + spring-boot-starter-web + ${spring.boot.version} + + + org.springframework.boot + spring-boot-starter-test + ${spring.boot.version} + + + + diff --git a/sentinel-demo/sentinel-demo-jax-rs/src/main/java/com/alibaba/csp/sentinel/demo/jaxrs/CustomExceptionMapper.java b/sentinel-demo/sentinel-demo-jax-rs/src/main/java/com/alibaba/csp/sentinel/demo/jaxrs/CustomExceptionMapper.java new file mode 100644 index 00000000..feb76a31 --- /dev/null +++ b/sentinel-demo/sentinel-demo-jax-rs/src/main/java/com/alibaba/csp/sentinel/demo/jaxrs/CustomExceptionMapper.java @@ -0,0 +1,38 @@ +/* + * Copyright 1999-2020 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.demo.jaxrs; + +import org.springframework.stereotype.Component; + +import javax.annotation.Priority; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +/** + * @author sea + */ +@Component +@Provider +@Priority(javax.ws.rs.Priorities.USER - 1) +public class CustomExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(Throwable exception) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("Unknown Server Error") + .build(); + } +} diff --git a/sentinel-demo/sentinel-demo-jax-rs/src/main/java/com/alibaba/csp/sentinel/demo/jaxrs/HelloEntity.java b/sentinel-demo/sentinel-demo-jax-rs/src/main/java/com/alibaba/csp/sentinel/demo/jaxrs/HelloEntity.java new file mode 100644 index 00000000..4b17ebee --- /dev/null +++ b/sentinel-demo/sentinel-demo-jax-rs/src/main/java/com/alibaba/csp/sentinel/demo/jaxrs/HelloEntity.java @@ -0,0 +1,62 @@ +/* + * Copyright 1999-2020 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.demo.jaxrs; + +/** + * @author sea + */ +public class HelloEntity { + + Long id; + + String msg; + + public HelloEntity() { + } + + public HelloEntity(String msg) { + this.msg = msg; + } + + public HelloEntity(Long id, String msg) { + this.id = id; + this.msg = msg; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + @Override + public String toString() { + return "HelloEntity{" + + "id=" + id + + ", msg='" + msg + '\'' + + '}'; + } +} diff --git a/sentinel-demo/sentinel-demo-jax-rs/src/main/java/com/alibaba/csp/sentinel/demo/jaxrs/HelloResource.java b/sentinel-demo/sentinel-demo-jax-rs/src/main/java/com/alibaba/csp/sentinel/demo/jaxrs/HelloResource.java new file mode 100644 index 00000000..58e59d64 --- /dev/null +++ b/sentinel-demo/sentinel-demo-jax-rs/src/main/java/com/alibaba/csp/sentinel/demo/jaxrs/HelloResource.java @@ -0,0 +1,63 @@ +/* + * Copyright 1999-2020 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.demo.jaxrs; + +import org.springframework.stereotype.Component; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * HelloResource + * @author sea + */ +@Path("/hello") +@Produces(MediaType.APPLICATION_JSON) +@Component +public class HelloResource { + + @GET + public HelloEntity sayHello() { + return new HelloEntity("hello"); + } + + @GET + @Path("/{id}") + public HelloEntity get(@PathParam(value = "id") Long id) { + return new HelloEntity(id, "hello"); + } + + @GET + @Path("/list") + public List getAll() { + return IntStream.rangeClosed(1, 1000) + .mapToObj(i -> new HelloEntity((long)i, "hello")) + .collect(Collectors.toList()); + } + + @Path("/ex") + @GET + @Produces({ MediaType.APPLICATION_JSON }) + public String exception() { + throw new RuntimeException("test exception mapper"); + } +} diff --git a/sentinel-demo/sentinel-demo-jax-rs/src/main/java/com/alibaba/csp/sentinel/demo/jaxrs/JaxRsClientDemo.java b/sentinel-demo/sentinel-demo-jax-rs/src/main/java/com/alibaba/csp/sentinel/demo/jaxrs/JaxRsClientDemo.java new file mode 100644 index 00000000..c273b310 --- /dev/null +++ b/sentinel-demo/sentinel-demo-jax-rs/src/main/java/com/alibaba/csp/sentinel/demo/jaxrs/JaxRsClientDemo.java @@ -0,0 +1,51 @@ +/* + * Copyright 1999-2020 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.demo.jaxrs; + +import com.alibaba.csp.sentinel.adapter.jaxrs.SentinelJaxRsClientTemplate; +import com.alibaba.csp.sentinel.util.function.Supplier; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.Response; +import java.util.concurrent.TimeUnit; + +/** + * @author sea + */ +public class JaxRsClientDemo { + + public static void main(String[] args) { + Client client = ClientBuilder.newBuilder() + .connectTimeout(3, TimeUnit.SECONDS) + .readTimeout(3, TimeUnit.SECONDS) + .build(); + + final String host = "http://127.0.0.1:8181"; + final String url = "/hello/1"; + String resourceName = "GET:" + url; + Response response = SentinelJaxRsClientTemplate.execute(resourceName, new Supplier() { + + @Override + public Response get() { + return client.target(host).path(url).request() + .get(); + } + }); + System.out.println(response.readEntity(HelloEntity.class)); + System.exit(0); + } +} diff --git a/sentinel-demo/sentinel-demo-jax-rs/src/main/java/com/alibaba/csp/sentinel/demo/jaxrs/JaxRsDemoApplication.java b/sentinel-demo/sentinel-demo-jax-rs/src/main/java/com/alibaba/csp/sentinel/demo/jaxrs/JaxRsDemoApplication.java new file mode 100644 index 00000000..460f29ee --- /dev/null +++ b/sentinel-demo/sentinel-demo-jax-rs/src/main/java/com/alibaba/csp/sentinel/demo/jaxrs/JaxRsDemoApplication.java @@ -0,0 +1,33 @@ +/* + * Copyright 1999-2020 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.demo.jaxrs; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + *

Add the JVM parameter to connect to the dashboard:

+ * {@code -Dcsp.sentinel.dashboard.server=127.0.0.1:8080 -Dproject.name=sentinel-demo-jax-rs} + * + * @author sea + */ +@SpringBootApplication +public class JaxRsDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(JaxRsDemoApplication.class); + } +} diff --git a/sentinel-demo/sentinel-demo-jax-rs/src/main/java/com/alibaba/csp/sentinel/demo/jaxrs/SentinelJaxRsConfig.java b/sentinel-demo/sentinel-demo-jax-rs/src/main/java/com/alibaba/csp/sentinel/demo/jaxrs/SentinelJaxRsConfig.java new file mode 100644 index 00000000..fbabc214 --- /dev/null +++ b/sentinel-demo/sentinel-demo-jax-rs/src/main/java/com/alibaba/csp/sentinel/demo/jaxrs/SentinelJaxRsConfig.java @@ -0,0 +1,32 @@ +/* + * Copyright 1999-2020 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.demo.jaxrs; + +import com.alibaba.csp.sentinel.adapter.jaxrs.SentinelJaxRsProviderFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author sea + */ +@Configuration(proxyBeanMethods = false) +public class SentinelJaxRsConfig { + + @Bean + public SentinelJaxRsProviderFilter sentinelJaxRsProviderFilter() { + return new SentinelJaxRsProviderFilter(); + } +} diff --git a/sentinel-demo/sentinel-demo-jax-rs/src/main/resources/application.properties b/sentinel-demo/sentinel-demo-jax-rs/src/main/resources/application.properties new file mode 100644 index 00000000..d636638d --- /dev/null +++ b/sentinel-demo/sentinel-demo-jax-rs/src/main/resources/application.properties @@ -0,0 +1 @@ +server.port=8181