diff --git a/transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionOutbox.java b/transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionOutbox.java index 51e0ab59..eb59f990 100644 --- a/transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionOutbox.java +++ b/transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionOutbox.java @@ -4,6 +4,8 @@ import java.time.Duration; import java.util.concurrent.Executor; import java.util.function.Supplier; + +import com.gruelbox.transactionoutbox.spi.AbstractProxyFactory; import lombok.ToString; import org.slf4j.MDC; import org.slf4j.event.Level; @@ -142,6 +144,7 @@ abstract class TransactionOutboxBuilder { protected TransactionManager transactionManager; protected Instantiator instantiator; protected Submitter submitter; + protected AbstractProxyFactory proxyFactory; protected Duration attemptFrequency; protected int blockAfterAttempts; protected int flushBatchSize; @@ -189,6 +192,16 @@ public TransactionOutboxBuilder submitter(Submitter submitter) { return this; } + /** + * @param proxyFactory Used for proxy object creation + * + * @return Builder. + */ + public TransactionOutboxBuilder proxyFactory(AbstractProxyFactory proxyFactory) { + this.proxyFactory = proxyFactory; + return this; + } + /** * @param attemptFrequency How often tasks should be re-attempted. This should be balanced with * {@link #flushBatchSize} and the frequency with which {@link #flush()} is called to diff --git a/transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionOutboxImpl.java b/transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionOutboxImpl.java index 6fdf46de..01acbb9f 100644 --- a/transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionOutboxImpl.java +++ b/transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionOutboxImpl.java @@ -5,6 +5,7 @@ import static java.time.temporal.ChronoUnit.MILLIS; import static java.time.temporal.ChronoUnit.MINUTES; +import com.gruelbox.transactionoutbox.spi.AbstractProxyFactory; import com.gruelbox.transactionoutbox.spi.ProxyFactory; import com.gruelbox.transactionoutbox.spi.Utils; import java.lang.reflect.InvocationTargetException; @@ -46,7 +47,7 @@ final class TransactionOutboxImpl implements TransactionOutbox, Validatable { private final Validator validator; private final Duration retentionThreshold; private final AtomicBoolean initialized = new AtomicBoolean(); - private final ProxyFactory proxyFactory = new ProxyFactory(); + private final AbstractProxyFactory proxyFactory; private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); @Override @@ -62,6 +63,7 @@ public void validate(Validator validator) { validator.notNull("clockProvider", clockProvider); validator.notNull("listener", listener); validator.notNull("retentionThreshold", retentionThreshold); + validator.notNull("proxyFactory", proxyFactory); } static TransactionOutboxBuilder builder() { @@ -431,7 +433,8 @@ public TransactionOutboxImpl build() { Utils.firstNonNull(listener, () -> TransactionOutboxListener.EMPTY), serializeMdc == null || serializeMdc, validator, - retentionThreshold == null ? Duration.ofDays(7) : retentionThreshold); + retentionThreshold == null ? Duration.ofDays(7) : retentionThreshold, + Utils.firstNonNull(proxyFactory, ProxyFactory::new)); validator.validate(impl); if (initializeImmediately == null || initializeImmediately) { impl.initialize(); diff --git a/transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/AbstractProxyFactory.java b/transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/AbstractProxyFactory.java new file mode 100644 index 00000000..edf24cb8 --- /dev/null +++ b/transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/AbstractProxyFactory.java @@ -0,0 +1,97 @@ +package com.gruelbox.transactionoutbox.spi; + +import com.gruelbox.transactionoutbox.MissingOptionalDependencyException; +import lombok.extern.slf4j.Slf4j; +import net.bytebuddy.TypeCache; +import org.objenesis.Objenesis; +import org.objenesis.ObjenesisStd; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.concurrent.Callable; +import java.util.function.BiFunction; + +import static java.lang.reflect.Proxy.newProxyInstance; +import static java.util.Optional.ofNullable; + +/** + * @author Ilya Viaznin + */ +@Slf4j +public abstract class AbstractProxyFactory { + + private final Objenesis objenesis = setupObjenesis(); + + private final TypeCache> byteBuddyCache = setupByteBuddyCache(); + + private static boolean hasDefaultConstructor(Class clazz) { + try { + clazz.getConstructor(); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + protected abstract Callable> byteBuddyProxyCallable(Class clazz); + + @SuppressWarnings({"unchecked", "cast"}) + public T createProxy(Class clazz, BiFunction processor) { + return clazz.isInterface() + // Fastest, we can just proxy an interface directly + ? (T) newProxyInstance(clazz.getClassLoader(), + new Class[]{clazz}, + (proxy, method, args) -> processor.apply(method, args)) + : constructProxy(clazz, processor, buildByteBuddyProxyClass(clazz)); + } + + protected T constructProxy(Class clazz, BiFunction processor, Class proxy) { + final T instance; + if (hasDefaultConstructor(clazz)) instance = Utils.uncheckedly(() -> proxy.getDeclaredConstructor().newInstance()); + else { + if (objenesis == null) throw new MissingOptionalDependencyException("org.objenesis", "objenesis"); + + var instantiator = objenesis.getInstantiatorOf(proxy); + instance = instantiator.newInstance(); + } + Utils.uncheck( + () -> { + var field = instance.getClass().getDeclaredField("handler"); + field.set( + instance, + (InvocationHandler) (proxy1, method, args) -> processor.apply(method, args)); + }); + + return instance; + } + + @SuppressWarnings({"unchecked", "cast"}) + protected Class buildByteBuddyProxyClass(Class clazz) { + return (Class) + ofNullable(byteBuddyCache) + .orElseThrow(() -> new MissingOptionalDependencyException("net.bytebuddy", "byte-buddy")) + .findOrInsert( + clazz.getClassLoader(), + clazz, + byteBuddyProxyCallable(clazz) + ); + } + + private TypeCache> setupByteBuddyCache() { + try { + return new TypeCache<>(TypeCache.Sort.WEAK); + } catch (NoClassDefFoundError error) { + log.info("ByteBuddy is not on the classpath, so only interfaces can be used with transaction-outbox"); + return null; + } + } + + private ObjenesisStd setupObjenesis() { + try { + return new ObjenesisStd(); + } catch (NoClassDefFoundError error) { + log.info("Objenesis is not on the classpath, so only interfaces or classes with default constructors can be used with transaction-outbox"); + return null; + } + } +} diff --git a/transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/LookupProxyFactory.java b/transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/LookupProxyFactory.java new file mode 100644 index 00000000..24e2fe23 --- /dev/null +++ b/transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/LookupProxyFactory.java @@ -0,0 +1,36 @@ +package com.gruelbox.transactionoutbox.spi; + +import lombok.SneakyThrows; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.description.modifier.Visibility; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.InvocationHandlerAdapter; +import net.bytebuddy.matcher.ElementMatchers; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.InvocationHandler; +import java.util.concurrent.Callable; + +/** + * Proxy factory that uses lookup load method in bytebuddy + *

+ * Use for JDK 17+ + * + * @author Ilya Viaznin + */ +public class LookupProxyFactory extends AbstractProxyFactory { + + @Override + @SneakyThrows + protected Callable> byteBuddyProxyCallable(Class clazz) { + var lookup = MethodHandles.privateLookupIn(clazz, MethodHandles.lookup()); + return () -> new ByteBuddy() + .subclass(clazz) + .defineField("handler", InvocationHandler.class, Visibility.PUBLIC) + .method(ElementMatchers.isDeclaredBy(clazz)) + .intercept(InvocationHandlerAdapter.toField("handler")) + .make() + .load(clazz.getClassLoader(), ClassLoadingStrategy.UsingLookup.of(lookup)) + .getLoaded(); + } +} diff --git a/transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/ProxyFactory.java b/transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/ProxyFactory.java index 4df3d877..aa72c081 100644 --- a/transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/ProxyFactory.java +++ b/transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/ProxyFactory.java @@ -1,111 +1,25 @@ package com.gruelbox.transactionoutbox.spi; -import com.gruelbox.transactionoutbox.MissingOptionalDependencyException; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.util.function.BiFunction; -import lombok.extern.slf4j.Slf4j; import net.bytebuddy.ByteBuddy; -import net.bytebuddy.TypeCache; -import net.bytebuddy.TypeCache.Sort; import net.bytebuddy.description.modifier.Visibility; import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; import net.bytebuddy.implementation.InvocationHandlerAdapter; import net.bytebuddy.matcher.ElementMatchers; -import org.objenesis.Objenesis; -import org.objenesis.ObjenesisStd; -import org.objenesis.instantiator.ObjectInstantiator; - -@Slf4j -public class ProxyFactory { - - private final Objenesis objenesis = setupObjenesis(); - private final TypeCache> byteBuddyCache = setupByteBuddyCache(); - - private static boolean hasDefaultConstructor(Class clazz) { - try { - clazz.getConstructor(); - return true; - } catch (NoSuchMethodException e) { - return false; - } - } - - private TypeCache> setupByteBuddyCache() { - try { - return new TypeCache<>(Sort.WEAK); - } catch (NoClassDefFoundError error) { - log.info( - "ByteBuddy is not on the classpath, so only interfaces can be used with transaction-outbox"); - return null; - } - } - private ObjenesisStd setupObjenesis() { - try { - return new ObjenesisStd(); - } catch (NoClassDefFoundError error) { - log.info( - "Objenesis is not on the classpath, so only interfaces or classes with default constructors can be used with transaction-outbox"); - return null; - } - } - - @SuppressWarnings({"unchecked", "cast"}) - public T createProxy(Class clazz, BiFunction processor) { - if (clazz.isInterface()) { - // Fastest - we can just proxy an interface directly - return (T) - Proxy.newProxyInstance( - clazz.getClassLoader(), - new Class[] {clazz}, - (proxy, method, args) -> processor.apply(method, args)); - } else { - Class proxy = buildByteBuddyProxyClass(clazz); - return constructProxy(clazz, processor, proxy); - } - } - - private T constructProxy( - Class clazz, BiFunction processor, Class proxy) { - final T instance; - if (hasDefaultConstructor(clazz)) { - instance = Utils.uncheckedly(() -> proxy.getDeclaredConstructor().newInstance()); - } else { - if (objenesis == null) { - throw new MissingOptionalDependencyException("org.objenesis", "objenesis"); - } - ObjectInstantiator instantiator = objenesis.getInstantiatorOf(proxy); - instance = instantiator.newInstance(); - } - Utils.uncheck( - () -> { - var field = instance.getClass().getDeclaredField("handler"); - field.set( - instance, - (InvocationHandler) (proxy1, method, args) -> processor.apply(method, args)); - }); - return instance; - } - - @SuppressWarnings({"unchecked", "cast"}) - private Class buildByteBuddyProxyClass(Class clazz) { - if (byteBuddyCache == null) { - throw new MissingOptionalDependencyException("net.bytebuddy", "byte-buddy"); +import java.lang.reflect.InvocationHandler; +import java.util.concurrent.Callable; + +public class ProxyFactory extends AbstractProxyFactory { + + @Override + protected Callable> byteBuddyProxyCallable(Class clazz) { + return () -> new ByteBuddy() + .subclass(clazz) + .defineField("handler", InvocationHandler.class, Visibility.PUBLIC) + .method(ElementMatchers.isDeclaredBy(clazz)) + .intercept(InvocationHandlerAdapter.toField("handler")) + .make() + .load(clazz.getClassLoader(), ClassLoadingStrategy.Default.INJECTION) + .getLoaded(); } - return (Class) - byteBuddyCache.findOrInsert( - clazz.getClassLoader(), - clazz, - () -> - new ByteBuddy() - .subclass(clazz) - .defineField("handler", InvocationHandler.class, Visibility.PUBLIC) - .method(ElementMatchers.isDeclaredBy(clazz)) - .intercept(InvocationHandlerAdapter.toField("handler")) - .make() - .load(clazz.getClassLoader(), ClassLoadingStrategy.Default.INJECTION) - .getLoaded()); - } } diff --git a/transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestLookupProxyGeneration.java b/transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestLookupProxyGeneration.java new file mode 100644 index 00000000..4e1a6aeb --- /dev/null +++ b/transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestLookupProxyGeneration.java @@ -0,0 +1,88 @@ +package com.gruelbox.transactionoutbox; + +import com.gruelbox.transactionoutbox.spi.LookupProxyFactory; +import com.gruelbox.transactionoutbox.spi.ProxyFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TestLookupProxyGeneration { + + private LookupProxyFactory proxyFactory; + + @BeforeEach + void setUp() { + proxyFactory = new LookupProxyFactory(); + } + + /** Reflection */ + @Test + void testReflection() { + AtomicBoolean called = new AtomicBoolean(); + Interface proxy = + proxyFactory.createProxy( + Interface.class, + (method, args) -> { + called.set(true); + return null; + }); + proxy.doThing(); + assertTrue(called.get()); + } + + /** ByteBuddy */ + @Test + void testByteBuddy() { + AtomicBoolean called = new AtomicBoolean(); + Child proxy = + proxyFactory.createProxy( + Child.class, + (method, args) -> { + called.set(true); + return null; + }); + proxy.doThing(); + assertTrue(called.get()); + } + + /** This fails without Objenesis. */ + @Test + void testObjensis() { + AtomicBoolean called = new AtomicBoolean(); + Parent proxy = + proxyFactory.createProxy( + Parent.class, + (method, args) -> { + called.set(true); + return null; + }); + proxy.doThing(); + assertTrue(called.get()); + } + + interface Interface { + void doThing(); + } + + static class Child { + void doThing() { + // No-op + } + } + + static class Parent { + + private final Child child; + + Parent(Child child) { + this.child = child; + } + + void doThing() { + // No-op + } + } +}