Skip to content

Commit

Permalink
feat: spring support
Browse files Browse the repository at this point in the history
  • Loading branch information
Citymonstret committed Oct 12, 2024
1 parent ada81cd commit 41ad97e
Show file tree
Hide file tree
Showing 14 changed files with 482 additions and 8 deletions.
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ library for introducing disruptions to your code to replicate an unstable live e
## Modules

- **core:** core disruptor API
- ~~**spring:** spring integration~~
- **spring:** spring integration
- **openfeign:** feign integration

## Example
Expand Down Expand Up @@ -56,4 +56,27 @@ Feign.builder()/*...*/.addCapability(capability)/*...*/;
Capability disruptorCapability() {
return DisruptorCapability(disruptor, "group");
}
```

### Spring

```java
import java.beans.BeanProperty;

@Configuration
public class YourConfig {

@Bean
Disruptor disruptor() {
return Disruptor.builder()/*...*/.build();
}
}

@Disrupt("group") // You may annotate a class...
public class YourService {

@Disrupt("other-group") //... or a method
public void yourMethod() {
}
}
```
6 changes: 0 additions & 6 deletions TODO.md

This file was deleted.

3 changes: 2 additions & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ dependencyResolutionManagement {
rootProject.name = "disruptor"

include(":core")
include(":openfeign")
include(":openfeign")
include(":spring")
24 changes: 24 additions & 0 deletions spring/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import org.springframework.boot.gradle.tasks.bundling.BootJar

plugins {
id("disruptor.base-conventions")
id("disruptor.publishing-conventions")
alias(libs.plugins.spring.plugin.boot)
}

plugins.apply("io.spring.dependency-management")

tasks.named<BootJar>("bootJar") {
enabled = false
}

dependencies {
api(projects.disruptor.core)
implementation(libs.spring.boot.autoconfigure)

testImplementation(libs.spring.boot.starter.test)
}

tasks.named<Test>("test") {
useJUnitPlatform()
}
62 changes: 62 additions & 0 deletions spring/src/main/java/org/incendo/disruptor/spring/Disrupt.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//
// MIT License
//
// Copyright (c) 2024 Incendo
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
package org.incendo.disruptor.spring;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.apiguardian.api.API;
import org.springframework.core.annotation.AliasFor;

/**
* Annotation placed on classes or methods to indicate that they should be disrupted.
*
* @since 1.0.0
*/
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@API(status = API.Status.STABLE, since = "1.0.0")
public @interface Disrupt {

/**
* Returns the group name.
*
* @return group name
*/
@AliasFor("group")
String value() default "";

/**
* Returns the group name.
*
* @return group name
*/
@AliasFor("value")
String group() default "";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//
// MIT License
//
// Copyright (c) 2024 Incendo
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
package org.incendo.disruptor.spring;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apiguardian.api.API;
import org.incendo.disruptor.DisruptionMode;
import org.incendo.disruptor.Disruptor;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Role;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;

import static org.springframework.beans.factory.config.BeanDefinition.ROLE_INFRASTRUCTURE;

@Role(ROLE_INFRASTRUCTURE)
@ConditionalOnBean(Disruptor.class)
@Component
@API(status = API.Status.INTERNAL, since = "1.0.0")
public class DisruptorAdvice implements MethodInterceptor, ApplicationListener<ApplicationReadyEvent> {

private static final Logger LOGGER = LoggerFactory.getLogger(DisruptorAdvice.class);

private @Nullable Disruptor disruptor;

@Override
public Object invoke(final MethodInvocation invocation) throws Throwable {
if (this.disruptor == null) {
return invocation.proceed();
}

// The annotation may either be on the class, or method level.
final String group;
final Disrupt methodAnnotation = AnnotationUtils.findAnnotation(invocation.getMethod(), Disrupt.class);

final Class<?> clazz;
if (invocation.getThis() != null) {
clazz = invocation.getThis().getClass();
} else {
clazz = invocation.getMethod().getDeclaringClass();
}

if (methodAnnotation == null) {
final Disrupt classAnnotation = AnnotationUtils.findAnnotation(clazz, Disrupt.class);
if (classAnnotation == null) {
return invocation.proceed();
}
group = classAnnotation.group();
} else {
group = methodAnnotation.group();
}

LOGGER.trace(
"Calling disruptor for method {} in class {} using group {}",
invocation.getMethod().getName(),
clazz.getCanonicalName(),
group
);

this.disruptor.disrupt(group, DisruptionMode.BEFORE);
final Object result = invocation.proceed();
this.disruptor.disrupt(group, DisruptionMode.AFTER);
return result;
}

@Override
public void onApplicationEvent(final ApplicationReadyEvent event) {
this.disruptor = event.getApplicationContext().getBean(Disruptor.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// MIT License
//
// Copyright (c) 2024 Incendo
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
package org.incendo.disruptor.spring;

import java.util.concurrent.atomic.AtomicBoolean;
import org.apiguardian.api.API;
import org.incendo.disruptor.Disruptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;

@ConditionalOnBean(Disruptor.class)
@Component
@API(status = API.Status.INTERNAL, since = "1.0.0")
public class DisruptorBeanPostProcessor implements BeanPostProcessor {

private static final Logger LOGGER = LoggerFactory.getLogger(DisruptorAdvice.class);

private final DisruptorAdvice disruptorAdvice;

/**
* Creates a new post processor.
*
* @param disruptorAdvice disruptor advice
*/
public DisruptorBeanPostProcessor(final DisruptorAdvice disruptorAdvice) {
this.disruptorAdvice = disruptorAdvice;
}

@Override
public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException {
// If there are no @Disrupt annotations then we're not interested.
if (!this.hasDisruptorAnnotation(bean)) {
return bean;
}

LOGGER.debug("Creating disruptor proxy for bean {}, class {}", bean, bean.getClass().getCanonicalName());

// If there are, however, then we want to create a new proxy which invokes the disruptor.
final ProxyFactory proxyFactory = new ProxyFactory(bean);
proxyFactory.addAdvice(this.disruptorAdvice);
return proxyFactory.getProxy(getClass().getClassLoader());
}

private boolean hasDisruptorAnnotation(final Object bean) {
if (AnnotationUtils.findAnnotation(bean.getClass(), Disrupt.class) != null) {
return true;
}
final AtomicBoolean annotationPresent = new AtomicBoolean(false);
ReflectionUtils.doWithMethods(bean.getClass(), method -> {
if (AnnotationUtils.findAnnotation(method, Disrupt.class) != null) {
annotationPresent.set(true);
}
});
return annotationPresent.get();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@NullMarked
package org.incendo.disruptor.spring;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
org.incendo.disruptor.spring.DisruptorAdvice
org.incendo.disruptor.spring.DisruptorBeanPostProcessor
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// MIT License
//
// Copyright (c) 2024 Incendo
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
package org.incendo.disruptor.test;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

@SpringBootTest
class DisruptorAdviceTest {

@Autowired private TestService testService;

@Test
void methodAnnotation() {
final RuntimeException runtimeException = assertThrows(
RuntimeException.class,
() -> this.testService.testMethod()
);
assertThat(runtimeException).hasMessageThat().isEqualTo("method");
}

@Test
void classAnnotation() {
final RuntimeException runtimeException = assertThrows(
RuntimeException.class,
() -> this.testService.otherMethod()
);
assertThat(runtimeException).hasMessageThat().isEqualTo("class");
}
}
Loading

0 comments on commit 41ad97e

Please sign in to comment.