Skip to content

Commit 00c4704

Browse files
authored
Changes to FT implementation to support interception of proxy methods (master) (helidon-io#4650)
1 parent 058ef43 commit 00c4704

12 files changed

Lines changed: 569 additions & 1 deletion

File tree

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Copyright (c) 2022 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.helidon.microprofile.faulttolerance;
18+
19+
import java.lang.annotation.Annotation;
20+
import java.lang.reflect.Method;
21+
import java.lang.reflect.Type;
22+
import java.util.LinkedHashSet;
23+
import java.util.List;
24+
import java.util.Set;
25+
26+
import jakarta.enterprise.inject.spi.AnnotatedConstructor;
27+
import jakarta.enterprise.inject.spi.AnnotatedField;
28+
import jakarta.enterprise.inject.spi.AnnotatedMethod;
29+
import jakarta.enterprise.inject.spi.AnnotatedParameter;
30+
import jakarta.enterprise.inject.spi.AnnotatedType;
31+
32+
import static java.util.Arrays.asList;
33+
34+
/**
35+
* Wrapper for a Java method that implements the {@link AnnotatedMethod} interface.
36+
* This wrapper is necessary to handle proxy methods, such as those created from
37+
* a RestClient interface.
38+
*/
39+
class FtAnnotatedMethod implements AnnotatedMethod<Object> {
40+
41+
private final Method method;
42+
43+
private final AnnotatedType<Object> type;
44+
45+
FtAnnotatedMethod(Method method) {
46+
this.method = method;
47+
this.type = new AnnotatedType<>() {
48+
private final Class<?> clazz = method.getDeclaringClass();
49+
50+
@Override
51+
@SuppressWarnings("unchecked")
52+
public Class<Object> getJavaClass() {
53+
return (Class<Object>) clazz;
54+
}
55+
56+
@Override
57+
public <T extends Annotation> T getAnnotation(Class<T> annotationType) {
58+
return clazz.getAnnotation(annotationType);
59+
}
60+
61+
@Override
62+
public Set<Annotation> getAnnotations() {
63+
return Set.of(clazz.getAnnotations());
64+
}
65+
66+
@Override
67+
public boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {
68+
return clazz.isAnnotationPresent(annotationType);
69+
}
70+
71+
@Override
72+
public Set<AnnotatedConstructor<Object>> getConstructors() {
73+
throw new IllegalStateException("Should not be called");
74+
}
75+
76+
@Override
77+
public Set<AnnotatedMethod<? super Object>> getMethods() {
78+
throw new IllegalStateException("Should not be called");
79+
}
80+
81+
@Override
82+
public Set<AnnotatedField<? super Object>> getFields() {
83+
throw new IllegalStateException("Should not be called");
84+
}
85+
86+
@Override
87+
public Type getBaseType() {
88+
throw new IllegalStateException("Should not be called");
89+
}
90+
91+
@Override
92+
public Set<Type> getTypeClosure() {
93+
throw new IllegalStateException("Should not be called");
94+
}
95+
};
96+
}
97+
98+
@Override
99+
public Method getJavaMember() {
100+
return method;
101+
}
102+
103+
@Override
104+
public AnnotatedType<Object> getDeclaringType() {
105+
return type;
106+
}
107+
108+
@Override
109+
public <T extends Annotation> T getAnnotation(Class<T> annotationType) {
110+
Set<T> set = getAnnotations(annotationType);
111+
return set.isEmpty() ? null : set.iterator().next();
112+
}
113+
114+
@Override
115+
public <T extends Annotation> Set<T> getAnnotations(Class<T> annotationType) {
116+
T[] annotationsByType = getJavaMember().getAnnotationsByType(annotationType);
117+
return new LinkedHashSet<>(asList(annotationsByType));
118+
}
119+
120+
@Override
121+
public boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {
122+
return getJavaMember().isAnnotationPresent(annotationType);
123+
}
124+
125+
public Set<Annotation> getAnnotations() {
126+
Annotation[] annotations = getJavaMember().getAnnotations();
127+
return new LinkedHashSet<>(asList(annotations));
128+
}
129+
130+
@Override
131+
public Type getBaseType() {
132+
throw new IllegalStateException("Should not be called");
133+
}
134+
135+
@Override
136+
public Set<Type> getTypeClosure() {
137+
throw new IllegalStateException("Should not be called");
138+
}
139+
140+
@Override
141+
public List<AnnotatedParameter<Object>> getParameters() {
142+
throw new IllegalStateException("Should not be called");
143+
}
144+
145+
@Override
146+
public boolean isStatic() {
147+
throw new IllegalStateException("Should not be called");
148+
}
149+
}

microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/MethodIntrospector.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ class MethodIntrospector {
7070
.stream()
7171
.filter(am -> am.getJavaMember().equals(method))
7272
.findFirst();
73-
this.annotatedMethod = annotatedMethodOptional.orElseThrow();
73+
this.annotatedMethod = annotatedMethodOptional.orElse(new FtAnnotatedMethod(method));
7474

7575
this.retry = isAnnotationEnabled(Retry.class) ? new RetryAntn(annotatedMethod) : null;
7676
this.circuitBreaker = isAnnotationEnabled(CircuitBreaker.class)

tests/integration/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
<module>config</module>
5555
<module>jep290</module>
5656
<module>mp-bean-validation</module>
57+
<module>restclient</module>
5758
</modules>
5859

5960
<profiles>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
Copyright (c) 2022 Oracle and/or its affiliates.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
18+
<project xmlns="http://maven.apache.org/POM/4.0.0"
19+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
20+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
21+
<parent>
22+
<artifactId>helidon-tests-integration</artifactId>
23+
<groupId>io.helidon.tests.integration</groupId>
24+
<version>3.0.1-SNAPSHOT</version>
25+
</parent>
26+
<modelVersion>4.0.0</modelVersion>
27+
28+
<artifactId>helidon-tests-integration-restclient</artifactId>
29+
<name>Helidon Integration Test RestClient</name>
30+
31+
<dependencies>
32+
<dependency>
33+
<groupId>io.helidon.microprofile.bundles</groupId>
34+
<artifactId>helidon-microprofile</artifactId>
35+
</dependency>
36+
<dependency>
37+
<groupId>io.helidon.microprofile.rest-client</groupId>
38+
<artifactId>helidon-microprofile-rest-client</artifactId>
39+
</dependency>
40+
<dependency>
41+
<groupId>org.jboss</groupId>
42+
<artifactId>jandex</artifactId>
43+
<scope>runtime</scope>
44+
<optional>true</optional>
45+
</dependency>
46+
<dependency>
47+
<groupId>org.junit.jupiter</groupId>
48+
<artifactId>junit-jupiter-api</artifactId>
49+
<scope>test</scope>
50+
</dependency>
51+
<dependency>
52+
<groupId>io.helidon.microprofile.tests</groupId>
53+
<artifactId>helidon-microprofile-tests-junit5</artifactId>
54+
<scope>test</scope>
55+
</dependency>
56+
</dependencies>
57+
58+
<build>
59+
<plugins>
60+
<plugin>
61+
<groupId>org.apache.maven.plugins</groupId>
62+
<artifactId>maven-dependency-plugin</artifactId>
63+
<executions>
64+
<execution>
65+
<id>copy-libs</id>
66+
</execution>
67+
</executions>
68+
</plugin>
69+
</plugins>
70+
</build>
71+
</project>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright (c) 2022 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.helidon.tests.integration.restclient;
18+
19+
import jakarta.json.Json;
20+
import jakarta.json.JsonBuilderFactory;
21+
import jakarta.json.JsonObject;
22+
import jakarta.ws.rs.GET;
23+
import jakarta.ws.rs.Path;
24+
import jakarta.ws.rs.Produces;
25+
import jakarta.ws.rs.core.MediaType;
26+
import java.util.Collections;
27+
28+
/**
29+
* A typical greet resource that only handles a single GET for a default message.
30+
*/
31+
@Path("/greet")
32+
public class GreetResource {
33+
34+
private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap());
35+
36+
@GET
37+
@Produces(MediaType.APPLICATION_JSON)
38+
public JsonObject getDefaultMessage() {
39+
return createResponse("World");
40+
}
41+
42+
private JsonObject createResponse(String who) {
43+
String msg = String.format("%s %s!", "Hello", who);
44+
45+
return JSON.createObjectBuilder()
46+
.add("message", msg)
47+
.build();
48+
}
49+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright (c) 2022 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.helidon.tests.integration.restclient;
18+
19+
import jakarta.json.JsonObject;
20+
import jakarta.ws.rs.GET;
21+
import jakarta.ws.rs.Path;
22+
import jakarta.ws.rs.Produces;
23+
import jakarta.ws.rs.core.MediaType;
24+
25+
import org.eclipse.microprofile.faulttolerance.Retry;
26+
import org.eclipse.microprofile.faulttolerance.Timeout;
27+
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
28+
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
29+
30+
/**
31+
* RestClient interface for a simple greet resource that includes a few FT annotations.
32+
*/
33+
@Path("/greet")
34+
@RegisterProvider(GreetResourceFilter.class)
35+
@RegisterRestClient(baseUri="http://localhost:8080")
36+
public interface GreetResourceClient {
37+
38+
@GET
39+
@Retry
40+
@Timeout(value = 3000)
41+
@Produces(MediaType.APPLICATION_JSON)
42+
JsonObject getDefaultMessage();
43+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright (c) 2022 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.helidon.tests.integration.restclient;
18+
19+
import jakarta.ws.rs.client.ClientRequestContext;
20+
import jakarta.ws.rs.client.ClientRequestFilter;
21+
import jakarta.ws.rs.core.Context;
22+
import jakarta.ws.rs.core.UriInfo;
23+
import java.io.IOException;
24+
import java.net.URI;
25+
26+
/**
27+
* A client request filter that replaces port 8080 by the ephemeral port allocated for the
28+
* webserver in each run. This is necessary since {@link GreetResourceClient} uses an annotation
29+
* to specify the base URI, and its value cannot be changed dynamically.
30+
*/
31+
public class GreetResourceFilter implements ClientRequestFilter {
32+
33+
@Context
34+
UriInfo uriInfo;
35+
36+
@Override
37+
public void filter(ClientRequestContext requestContext) throws IOException {
38+
URI uri = requestContext.getUri();
39+
String fixedUri = uri.toString().replace("8080", extractDynamicPort());
40+
requestContext.setUri(URI.create(fixedUri));
41+
}
42+
43+
private String extractDynamicPort() {
44+
String uriString = uriInfo.getBaseUri().toString();
45+
int k = uriString.lastIndexOf(":");
46+
int j = uriString.indexOf("/", k);
47+
return uriString.substring(k + 1, j);
48+
}
49+
}

0 commit comments

Comments
 (0)