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 jax-rs #1396

Merged
merged 4 commits into from
Apr 23, 2020
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions sentinel-adapter/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<module>sentinel-spring-cloud-gateway-adapter</module>
<module>sentinel-spring-webmvc-adapter</module>
<module>sentinel-zuul2-adapter</module>
<module>sentinel-jax-rs-adapter</module>
</modules>

<dependencyManagement>
Expand Down
41 changes: 41 additions & 0 deletions sentinel-adapter/sentinel-jax-rs-adapter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# 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
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-jax-rs-adapter</artifactId>
<version>x.y.z</version>
</dependency>
```

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();
}
}
```

For jax-rs client, register `SentinelJaxRsClientFilter` when build Client

```
Client client = ClientBuilder.newClient()
.register(new SentinelJaxRsClientFilter());
```

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`.

The `RequestOriginParser` interface is useful for extracting request origin (e.g. IP or appName from HTTP Header)
from HTTP request. You can implement your own `RequestOriginParser` and register to `SentinelJaxRsConfig`.
74 changes: 74 additions & 0 deletions sentinel-adapter/sentinel-jax-rs-adapter/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?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">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-adapter</artifactId>
<version>1.8.0-SNAPSHOT</version>
</parent>

<artifactId>sentinel-jax-rs-adapter</artifactId>
<packaging>jar</packaging>

<properties>
<javax.ws.rs-api.version>2.1.1</javax.ws.rs-api.version>
</properties>

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

<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
<version>${javax.ws.rs-api.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.6.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.2.6.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>4.3.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-spring-boot-starter</artifactId>
<version>4.5.1.Final</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<forkMode>always</forkMode>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -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;


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<Throwable> {

@Override
public Response toResponse(Throwable exception) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(exception.getMessage())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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.Entry;
import com.alibaba.csp.sentinel.EntryType;
import com.alibaba.csp.sentinel.ResourceTypeConstants;
import com.alibaba.csp.sentinel.SphU;
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.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.client.ClientResponseContext;
import javax.ws.rs.client.ClientResponseFilter;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.Context;
import java.io.IOException;

/**
* @author sea
*/
public class SentinelJaxRsClientFilter implements ClientRequestFilter, ClientResponseFilter {

private static final String SENTINEL_JAX_RS_CLIENT_ENTRY_PROPERTY = "sentinel_jax_rs_client_entry_property";

@Context
private ResourceInfo resourceInfo;


@Override
public void filter(ClientRequestContext requestContext) throws IOException {
try {
String resourceName = getResourceName(requestContext);

if (StringUtil.isNotEmpty(resourceName)) {
Entry entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.OUT);

requestContext.setProperty(SENTINEL_JAX_RS_CLIENT_ENTRY_PROPERTY, entry);
}
} catch (BlockException e) {
try {
requestContext.abortWith(SentinelJaxRsConfig.getJaxRsFallback().fallbackResponse(requestContext.getUri().toString(), e));
} finally {
ContextUtil.exit();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For client side we don't need to exit the context here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, in the latest commit, the original SentinelJaxRsClientFilter is replaced by SentinelJaxRsClientTemplate to handle the exception and exit entry.

}
}
}

@Override
public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
Entry entry = (Entry) requestContext.getProperty(SENTINEL_JAX_RS_CLIENT_ENTRY_PROPERTY);
if (entry != null) {
entry.exit();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will the response filter method still be executed when unexpected error occurs? We need to guarantee the Entry exited finally.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for provider side

according to 3.3.4 Exceptions of jaxrs-2_1-final-spec:

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:

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.

in case of misuse, maybe a default exception mapper of Throwable can be provided by default, then the response filter can be called.

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:

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).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any similar problems on client side?

Copy link
Contributor Author

@seasidesky seasidesky Apr 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

client side may have similar problems, according to 4.4 Exception Mapping Providers of jaxrs-2_1-final-spec

Note that exception mapping providers are not supported as part of the Client API.

I introduce SentinelJaxRsClientTemplate to replace the original SentinelJaxRsClientFilter to fix it.

}
requestContext.removeProperty(SENTINEL_JAX_RS_CLIENT_ENTRY_PROPERTY);
}

public String getResourceName(ClientRequestContext requestContext) {
return SentinelJaxRsConfig.getResourceNameParser().parse(requestContext);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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() {
}
}
Loading