generated from creek-service/multi-module-template
-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
6 changed files
with
384 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
[![javadoc](https://javadoc.io/badge2/org.creekservice/creek-observability-patterns/javadoc.svg)](https://javadoc.io/doc/org.creekservice/creek-observability-patterns) | ||
|
||
# Creek Observability Patterns | ||
|
||
Contains helpers and utility code to help implement good observability in your code. | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
/* | ||
* Copyright 2022-2023 Creek Contributors (https://github.com/creek-service) | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
plugins { | ||
`java-library` | ||
} | ||
|
||
dependencies { | ||
} |
166 changes: 166 additions & 0 deletions
166
...s/src/main/java/org/creekservice/api/observability/patterns/CompositeObserverBuilder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
/* | ||
* Copyright 2023 Creek Contributors (https://github.com/creek-service) | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.creekservice.api.observability.patterns; | ||
|
||
import java.lang.invoke.MethodHandle; | ||
import java.lang.invoke.MethodHandles; | ||
import java.lang.reflect.Method; | ||
import java.lang.reflect.Proxy; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
import java.util.function.Function; | ||
import java.util.stream.Collectors; | ||
|
||
/** | ||
* Easily build a chain of multiple <i>Observer</i> / <i>Listener</i> implementations. | ||
* | ||
* <p>This encourages decomposition and decoupling of functionality. | ||
* | ||
* <p>Given an interface {@code MyListener}, this class can be used to chain multiple listeners | ||
* together, without the need to write any custom code. | ||
* | ||
* <pre>{@code | ||
* MyListener composite = CompositeObserverBuilder.builder(MyListener.class, listener1) | ||
* .observer(listener2) | ||
* ... | ||
* .observer(listenerN) | ||
* .build() | ||
* }</pre> | ||
* | ||
* @param <Observer> the observer interface type | ||
*/ | ||
public final class CompositeObserverBuilder<Observer> { | ||
|
||
private final Class<Observer> observerClass; | ||
private final List<Observer> observers = new ArrayList<>(); | ||
|
||
/** | ||
* Create a builder | ||
* | ||
* @param observerClass the observer interface type. | ||
* @param observer the first observer to call. | ||
* @return the composite builder | ||
* @param <Observer> the type of the observer interface. | ||
*/ | ||
public static <Observer> CompositeObserverBuilder<Observer> builder( | ||
final Class<Observer> observerClass, final Observer observer) { | ||
if (!observerClass.isInterface()) { | ||
throw new IllegalArgumentException("observerClass must be an interface"); | ||
} | ||
validateMethodsAreVoidReturnType(observerClass); | ||
return new CompositeObserverBuilder<>(observerClass, observer); | ||
} | ||
|
||
private CompositeObserverBuilder(final Class<Observer> observerClass, final Observer observer) { | ||
this.observerClass = observerClass; | ||
add(observer); | ||
} | ||
|
||
/** | ||
* Add another observer to the chain | ||
* | ||
* @param observer the observer to add. | ||
* @return self, to allow chaining. | ||
*/ | ||
public CompositeObserverBuilder<Observer> add(final Observer observer) { | ||
this.observers.add(Objects.requireNonNull(observer, "observer")); | ||
return this; | ||
} | ||
|
||
/** | ||
* @return the composite observer implementation. | ||
*/ | ||
@SuppressWarnings("unchecked") | ||
public Observer build() { | ||
try { | ||
final Map<Method, MethodHandle> handles = methodHandles(observerClass); | ||
return (Observer) | ||
Proxy.newProxyInstance( | ||
getClass().getClassLoader(), | ||
new Class<?>[] {observerClass}, | ||
(proxy, method, args) -> { | ||
if (method.getName().equals("toString")) { | ||
return "Proxy composite for " + observerClass.getName(); | ||
} | ||
if (method.getName().equals("equals")) { | ||
return this == proxy; | ||
} | ||
if (method.getName().equals("hashCode")) { | ||
return observerClass.getName().hashCode(); | ||
} | ||
|
||
for (Observer delegate : observers) { | ||
final MethodHandle methodHandle = handles.get(method); | ||
methodHandle.bindTo(delegate).invokeWithArguments(args); | ||
} | ||
return null; | ||
}); | ||
} catch (Exception e) { | ||
throw new CompositeCreationFailedException( | ||
"failed to generate composite observer for interface: " + observerClass, e); | ||
} | ||
} | ||
|
||
private Map<Method, MethodHandle> methodHandles(final Class<?> observerClass) { | ||
final Map<Method, MethodHandle> handles = | ||
Arrays.stream(observerClass.getDeclaredMethods()) | ||
.collect( | ||
Collectors.toMap( | ||
Function.identity(), | ||
m -> { | ||
try { | ||
return MethodHandles.publicLookup().unreflect(m); | ||
} catch (IllegalAccessException e) { | ||
throw new CompositeCreationFailedException( | ||
"Unable to unreflect method", e); | ||
} | ||
})); | ||
|
||
for (Class<?> extending : observerClass.getInterfaces()) { | ||
handles.putAll(methodHandles(extending)); | ||
} | ||
return handles; | ||
} | ||
|
||
private static void validateMethodsAreVoidReturnType(final Class<?> observerClass) { | ||
final List<Method> invalidMethods = | ||
Arrays.stream(observerClass.getDeclaredMethods()) | ||
.filter(method -> !method.getReturnType().equals(void.class)) | ||
.collect(Collectors.toList()); | ||
|
||
if (!invalidMethods.isEmpty()) { | ||
throw new UnsupportedOperationException( | ||
"Only observer interfaces, where all methods have a void return type, are" | ||
+ " supported. Interface: " | ||
+ observerClass | ||
+ " invalidMethods: " | ||
+ invalidMethods); | ||
} | ||
|
||
for (Class<?> extending : observerClass.getInterfaces()) { | ||
validateMethodsAreVoidReturnType(extending); | ||
} | ||
} | ||
|
||
private static class CompositeCreationFailedException extends RuntimeException { | ||
CompositeCreationFailedException(final String message, final Throwable cause) { | ||
super(message, cause); | ||
} | ||
} | ||
} |
185 changes: 185 additions & 0 deletions
185
...c/test/java/org/creekservice/api/observability/patterns/CompositeObserverBuilderTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
/* | ||
* Copyright 2023 Creek Contributors (https://github.com/creek-service) | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.creekservice.api.observability.patterns; | ||
|
||
import static org.hamcrest.CoreMatchers.notNullValue; | ||
import static org.hamcrest.MatcherAssert.assertThat; | ||
import static org.hamcrest.Matchers.is; | ||
import static org.junit.jupiter.api.Assertions.assertThrows; | ||
import static org.mockito.Mockito.inOrder; | ||
import static org.mockito.Mockito.mock; | ||
import static org.mockito.Mockito.verify; | ||
|
||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.ExtendWith; | ||
import org.mockito.InOrder; | ||
import org.mockito.Mock; | ||
import org.mockito.junit.jupiter.MockitoExtension; | ||
|
||
@ExtendWith(MockitoExtension.class) | ||
class CompositeObserverBuilderTest { | ||
|
||
@Mock Observer observer1; | ||
@Mock Observer observer2; | ||
|
||
@Test | ||
public void shouldDelegateToSingle() { | ||
// Given: | ||
final Observer result = CompositeObserverBuilder.builder(Observer.class, observer1).build(); | ||
|
||
// When: | ||
result.foo("text"); | ||
|
||
// Then: | ||
verify(observer1).foo("text"); | ||
} | ||
|
||
@Test | ||
public void shouldDelegateToMultipleInOrder() { | ||
// Given: | ||
final Observer result = | ||
CompositeObserverBuilder.builder(Observer.class, observer1).add(observer2).build(); | ||
|
||
// When: | ||
result.foo("text"); | ||
|
||
// Then: | ||
final InOrder inOrder = inOrder(observer1, observer2); | ||
inOrder.verify(observer1).foo("text"); | ||
inOrder.verify(observer2).foo("text"); | ||
} | ||
|
||
@Test | ||
public void shouldBlowUpIfObserverTypeNotInterface() { | ||
// When: | ||
final Exception e = | ||
assertThrows( | ||
IllegalArgumentException.class, | ||
() -> | ||
CompositeObserverBuilder.builder( | ||
CompositeObserverBuilderTest.class, this)); | ||
|
||
// Then: | ||
assertThat(e.getMessage(), is("observerClass must be an interface")); | ||
} | ||
|
||
@Test | ||
public void shouldBlowUpIfYouTryDelegateToNull() { | ||
// When: | ||
final Exception e = | ||
assertThrows( | ||
NullPointerException.class, | ||
() -> CompositeObserverBuilder.builder(Observer.class, null)); | ||
|
||
// Then: | ||
assertThat(e.getMessage(), is("observer")); | ||
} | ||
|
||
@Test | ||
public void shouldBlowUpIfYouTryToAddNullObserver() { | ||
// Given: | ||
final CompositeObserverBuilder<Observer> builder = | ||
CompositeObserverBuilder.builder(Observer.class, observer1); | ||
|
||
// When: | ||
final Exception e = assertThrows(NullPointerException.class, () -> builder.add(null)); | ||
|
||
// Then: | ||
assertThat(e.getMessage(), is("observer")); | ||
} | ||
|
||
@Test | ||
public void shouldBlowUpIfInterfaceHasNonVoidReturnMethods() { | ||
// When: | ||
final Exception e = | ||
assertThrows( | ||
UnsupportedOperationException.class, | ||
() -> CompositeObserverBuilder.builder(NotAnObserver.class, mock())); | ||
|
||
// Then: | ||
assertThat( | ||
e.getMessage(), | ||
is( | ||
"Only observer interfaces, where all methods have a void return type, are" | ||
+ " supported. Interface: interface" | ||
+ " org.creekservice.api.observability.patterns.CompositeObserverBuilderTest$NotAnObserver" | ||
+ " invalidMethods: [public abstract int" | ||
+ " org.creekservice.api.observability.patterns.CompositeObserverBuilderTest$NotAnObserver.foo(java.lang.String)]")); | ||
} | ||
|
||
@Test | ||
public void shouldHandleInterfaceExtensionWithoutBlowingUp() { | ||
// Given: | ||
final ExtendingExtendingObserver observer1 = mock(); | ||
|
||
final ExtendingExtendingObserver observer = | ||
CompositeObserverBuilder.builder(ExtendingExtendingObserver.class, observer1) | ||
.build(); | ||
|
||
// When: | ||
observer.foo("blah"); | ||
observer.bar(); | ||
observer.lar(); | ||
|
||
// Then: | ||
verify(observer1).foo("blah"); | ||
verify(observer1).bar(); | ||
verify(observer1).lar(); | ||
} | ||
|
||
@SuppressWarnings("ResultOfMethodCallIgnored") | ||
@Test | ||
public void shouldNotBlowUpOnEquals() { | ||
CompositeObserverBuilder.builder(ExtendingExtendingObserver.class, mock()) | ||
.build() | ||
.equals(new Object()); | ||
} | ||
|
||
@SuppressWarnings("ResultOfMethodCallIgnored") | ||
@Test | ||
public void shouldNotBlowUpOnHashCode() { | ||
CompositeObserverBuilder.builder(ExtendingExtendingObserver.class, mock()) | ||
.build() | ||
.hashCode(); | ||
} | ||
|
||
@Test | ||
public void shouldNotBlowUpOnToString() { | ||
assertThat( | ||
CompositeObserverBuilder.builder(ExtendingExtendingObserver.class, mock()) | ||
.build() | ||
.toString(), | ||
notNullValue()); | ||
} | ||
|
||
public interface Observer { | ||
|
||
void foo(String message); | ||
} | ||
|
||
public interface ExtendingObserver extends Observer { | ||
void bar(); | ||
} | ||
|
||
public interface ExtendingExtendingObserver extends ExtendingObserver { | ||
void lar(); | ||
} | ||
|
||
@SuppressWarnings("unused") | ||
public interface NotAnObserver { | ||
int foo(String message); | ||
} | ||
} |
Oops, something went wrong.