diff --git a/build.gradle b/build.gradle index 15710c79e..03df3d454 100644 --- a/build.gradle +++ b/build.gradle @@ -6,11 +6,12 @@ buildscript { } dependencies { - classpath "com.palantir:gradle-baseline-java:${baselineVersion}" - classpath "com.palantir:jacoco-coverage:${jacocoCoverageVersion}" + classpath "com.github.jengelman.gradle.plugins:shadow:${shadowVersion}" classpath "com.palantir.exartisi:gradle-exartisi-plugin:${exartisiVersion}" classpath "com.palantir.gradle.gitversion:gradle-git-version:${gitVersionVersion}" classpath "com.palantir.internalpublish:gradle-internal-publish-plugin:${internalPublishVersion}" + classpath "com.palantir:gradle-baseline-java:${baselineVersion}" + classpath "com.palantir:jacoco-coverage:${jacocoCoverageVersion}" classpath "gradle.plugin.com.palantir.configurationresolver:gradle-configuration-resolver-plugin:${configurationResolverVersion}" classpath "gradle.plugin.org.inferred:gradle-processors:${processorsVersion}" } diff --git a/gradle.properties b/gradle.properties index 0525ee2be..506cb5272 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,6 +3,7 @@ javaVersion = 1.8 ## Compile braveVersion = 3.11.1 caffeineVersion = 2.0.3 +cglibVersion = 3.2.4 dropwizardMetricsVersion = 3.1.2 findbugsVersion = 3.0.0 guavaVersion = 18.0 @@ -10,6 +11,7 @@ hdrHistogramMetricsReservoirVersion = 1.1.2 hdrHistogramVersion = 2.1.9 httpRemotingVersion = 1.0.0 immutablesVersion = 2.1.10 +objenesisVersion = 2.4 slf4jVersion = 1.7.12 ## Testing @@ -25,6 +27,7 @@ logbackClassicVersion = 1.1.2 # Build System antipatternsVersion = 1.0-beta.5 +aptGradleVersion = 0.9 baselineVersion = 0.7.1 configurationResolverVersion = 0.1.0 exartisiVersion = 1.2.2 @@ -32,5 +35,7 @@ gitVersionVersion = 0.5.2 internalPublishVersion = 0.1.7 jacocoCoverageVersion = 0.4.0 javaDistributionVersion = 0.7.2 +jmhGradleVersion = 0.3.1 launchConfigVersion = 0.3.3 processorsVersion = 1.2.1 +shadowVersion = 1.2.4 diff --git a/tritium-jmh/build.gradle b/tritium-jmh/build.gradle index 9286527d2..ff72ba832 100644 --- a/tritium-jmh/build.gradle +++ b/tritium-jmh/build.gradle @@ -7,17 +7,17 @@ buildscript { url 'https://plugins.gradle.org/m2/' } } + dependencies { + classpath "me.champeau.gradle:jmh-gradle-plugin:${jmhGradleVersion}" + classpath "net.ltgt.gradle:gradle-apt-plugin:${aptGradleVersion}" + } } -plugins { - id 'java' - id 'com.github.johnrengelman.shadow' version '1.2.3' - id 'me.champeau.gradle.jmh' version '0.3.1' - id 'net.ltgt.apt' version '0.9' -} - +apply plugin: 'java' +apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'com.palantir.git-version' apply plugin: 'me.champeau.gradle.jmh' +apply plugin: 'net.ltgt.apt' apply from: "${rootDir}/gradle/java.gradle" sourceCompatibility = '1.8' diff --git a/tritium-lib/build.gradle b/tritium-lib/build.gradle index 3d1275334..cc346bb30 100644 --- a/tritium-lib/build.gradle +++ b/tritium-lib/build.gradle @@ -1,12 +1,16 @@ apply plugin: 'com.palantir.publish-jar' +apply plugin: 'com.github.johnrengelman.shadow' dependencies { compile project(":tritium-api") compile project(":tritium-core") compile project(":tritium-metrics") compile project(":tritium-slf4j") + + compile "cglib:cglib-nodep:${cglibVersion}" compile "com.google.code.findbugs:annotations:${findbugsVersion}" compile "com.google.guava:guava:${guavaVersion}" + compile "org.objenesis:objenesis:${objenesisVersion}" compile "org.slf4j:slf4j-api:${slf4jVersion}" processor "org.immutables:value:${immutablesVersion}" @@ -22,3 +26,24 @@ dependencies { testCompile "org.mockito:mockito-all:${mockitoVersion}" testCompile "org.slf4j:slf4j-simple:${slf4jVersion}" } + +shadowJar { + mergeServiceFiles() + + dependencies { + // Exclude Guava, dropwizard metrics, and slf4j + exclude 'com/google/common/**' + exclude 'com/codahale/metrics/**' + exclude 'org/slf4j/**' + } + + relocate('net.sf.cglib', 'com.palantir.tritium.shaded.net.sf.cglib') + relocate('org.HdrHistogram', 'com.palantir.tritium.shaded.org.HdrHistogram') + relocate('org.mpierce', 'com.palantir.tritium.shaded.org.mpierce') + relocate('org.objenesis', 'com.palantir.tritium.shaded.org.objenesis') + +} + +publishing.publications.nebula { + artifact(shadowJar) +} diff --git a/tritium-lib/src/main/java/com/palantir/tritium/proxy/Instrumentation.java b/tritium-lib/src/main/java/com/palantir/tritium/proxy/Instrumentation.java index 3c6dccd47..35d9b919d 100644 --- a/tritium-lib/src/main/java/com/palantir/tritium/proxy/Instrumentation.java +++ b/tritium-lib/src/main/java/com/palantir/tritium/proxy/Instrumentation.java @@ -27,8 +27,13 @@ import com.palantir.tritium.event.log.LoggingLevel; import com.palantir.tritium.event.metrics.MetricsInvocationEventHandler; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.lang.reflect.Method; import java.util.Collections; import java.util.List; +import net.sf.cglib.proxy.Callback; +import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.InvocationHandler; +import org.objenesis.ObjenesisHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,18 +46,27 @@ private Instrumentation() { throw new UnsupportedOperationException(); } - public static T wrap( - final Class interfaceClass, + static T wrap( + final Class type, final U delegate, final List> handlers) { - checkNotNull(interfaceClass); - checkNotNull(delegate); - checkNotNull(handlers); + checkNotNull(type, "type"); + checkNotNull(delegate, "delegate"); + checkNotNull(handlers, "handlers"); if (handlers.isEmpty()) { return delegate; } + if (type.isInterface()) { + return createInterfaceProxy(type, delegate, handlers); + } else { + return createConcreteProxy(type, delegate, handlers); + } + } + + private static T createInterfaceProxy(Class interfaceClass, final U delegate, + final List> handlers) { return Proxies.newProxy(interfaceClass, delegate, new InvocationEventProxy(handlers) { @Override @@ -62,6 +76,21 @@ U getDelegate() { }); } + private static T createConcreteProxy(Class interfaceClass, + final U delegate, + final List> handlers) { + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(interfaceClass); + enhancer.setCallbackType(InvocationEventHandlerAdapter.class); + // use objenesis so that we can proxy classes which do not have a null constructor + @SuppressWarnings("unchecked") + Class newClass = (Class) enhancer.createClass(); + Enhancer.registerCallbacks(newClass, new Callback[] { + InvocationEventHandlerAdapter.create(delegate, handlers) + }); + return ObjenesisHelper.newInstance(newClass); + } + /** * Return an instrumented proxy of the specified service interface and delegate that records aggregated invocation * metrics and performance trace logging. @@ -75,9 +104,9 @@ U getDelegate() { @Deprecated public static T instrument(Class serviceInterface, U delegate, MetricRegistry metricRegistry) { return builder(serviceInterface, delegate) - .withMetrics(metricRegistry) - .withPerformanceTraceLogging() - .build(); + .withMetrics(metricRegistry) + .withPerformanceTraceLogging() + .build(); } public static Logger getPerformanceLoggerForInterface(Class serviceInterface) { @@ -136,4 +165,27 @@ public T build() { } } + private static class InvocationEventHandlerAdapter implements InvocationHandler { + private InvocationEventProxy proxy; + + InvocationEventHandlerAdapter(InvocationEventProxy proxy) { + this.proxy = proxy; + } + + static InvocationEventHandlerAdapter create(final T delegate, + List> handlers) { + return new InvocationEventHandlerAdapter( + new InvocationEventProxy(handlers) { + @Override + T getDelegate() { + return delegate; + } + }); + } + + @Override + public Object invoke(Object instance, Method method, Object[] args) throws Throwable { + return proxy.invoke(instance, method, args); + } + } } diff --git a/tritium-lib/src/test/java/com/palantir/tritium/proxy/InstrumentationTest.java b/tritium-lib/src/test/java/com/palantir/tritium/proxy/InstrumentationTest.java index 4585cf9f6..aca144834 100644 --- a/tritium-lib/src/test/java/com/palantir/tritium/proxy/InstrumentationTest.java +++ b/tritium-lib/src/test/java/com/palantir/tritium/proxy/InstrumentationTest.java @@ -16,15 +16,16 @@ package com.palantir.tritium.proxy; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import com.codahale.metrics.ConsoleReporter; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Slf4jReporter; import com.codahale.metrics.Slf4jReporter.LoggingLevel; @@ -41,13 +42,15 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; import java.util.Collections; -import java.util.Set; import java.util.SortedMap; -import org.hamcrest.CoreMatchers; -import org.hamcrest.Matchers; +import org.junit.After; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import org.slf4j.Logger; +@RunWith(MockitoJUnitRunner.class) public class InstrumentationTest { private static final String EXPECTED_METRIC_NAME = TestInterface.class.getName() + ".test"; @@ -55,14 +58,25 @@ public class InstrumentationTest { // Exceed the HotSpot JIT thresholds private static final int INVOCATION_ITERATIONS = 1500000; + @Mock + private InvocationEventHandler mockHandler; + + private MetricRegistry metrics = MetricRegistries.createWithHdrHistogramReservoirs(); + + @After + public void after() throws Exception { + ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics).build(); + reporter.report(); + reporter.close(); + } @Test public void testEmptyHandlers() { TestInterface delegate = new TestImplementation(); TestInterface instrumented = Instrumentation.wrap(TestInterface.class, delegate, Collections.>emptyList()); - assertThat(instrumented, is(equalTo(delegate))); - assertThat(Proxy.isProxyClass(instrumented.getClass()), equalTo(false)); + assertThat(instrumented).isEqualTo(delegate); + assertThat(Proxy.isProxyClass(instrumented.getClass())).isFalse(); } @Test @@ -76,23 +90,22 @@ public void testBuilder() { .withPerformanceTraceLogging() .build(); - assertThat(delegate.invocationCount(), equalTo(0)); - assertThat(metricRegistry.getTimers().get(Runnable.class.getName()), nullValue()); + assertThat(delegate.invocationCount()).isEqualTo(0); + assertThat(metricRegistry.getTimers().get(Runnable.class.getName())).isNull(); instrumentedService.test(); - assertThat(delegate.invocationCount(), equalTo(1)); + assertThat(delegate.invocationCount()).isEqualTo(1); SortedMap timers = metricRegistry.getTimers(); - assertThat(timers.keySet(), hasSize(1)); - assertThat(timers.keySet(), Matchers.>equalTo(ImmutableSet.of(EXPECTED_METRIC_NAME))); - assertThat(timers.get(EXPECTED_METRIC_NAME), notNullValue()); - assertThat(timers.get(EXPECTED_METRIC_NAME).getCount(), equalTo(1L)); + assertThat(timers.keySet()).hasSize(1); + assertThat(timers.keySet()).isEqualTo(ImmutableSet.of(EXPECTED_METRIC_NAME)); + assertThat(timers.get(EXPECTED_METRIC_NAME)).isNotNull(); + assertThat(timers.get(EXPECTED_METRIC_NAME).getCount()).isEqualTo(1); executeManyTimes(instrumentedService, INVOCATION_ITERATIONS); Slf4jReporter.forRegistry(metricRegistry).withLoggingLevel(LoggingLevel.INFO).build().report(); - assertThat(Long.valueOf(timers.get(EXPECTED_METRIC_NAME).getCount()).intValue(), - equalTo(delegate.invocationCount())); + assertThat(timers.get(EXPECTED_METRIC_NAME).getCount()).isEqualTo(delegate.invocationCount()); assertTrue(timers.get(EXPECTED_METRIC_NAME).getSnapshot().getMax() >= 0L); Slf4jReporter.forRegistry(metricRegistry).withLoggingLevel(LoggingLevel.INFO).build().report(); @@ -137,8 +150,8 @@ public void testCheckedExceptions() { instrumentedService.throwsCheckedException(); fail("Expected exception"); } catch (Exception expected) { - assertThat(expected, CoreMatchers.instanceOf(TestImplementation.TestException.class)); - assertThat(expected.getCause(), equalTo(null)); + assertThat(expected).isInstanceOf(TestImplementation.TestException.class); + assertThat(expected.getCause()).isNull(); } } } @@ -158,13 +171,46 @@ public void testThrowables() { instrumentedService.throwsThrowable(); fail("Expected throwable"); } catch (Throwable throwable) { - assertThat(throwable, CoreMatchers.instanceOf(AssertionError.class)); - assertThat(throwable.getCause(), equalTo(null)); + assertThat(throwable).isInstanceOf(AssertionError.class); + assertThat(throwable.getCause()).isNull(); } } } } + @Test + public void testWrapConcreteType() throws Exception { + when(mockHandler.isEnabled()).thenReturn(true); + Number instrumentedFourtyTwo = Instrumentation.builder(Number.class, Integer.valueOf(42)) + .withMetrics(metrics) + .withHandler(mockHandler) + .build(); + + assertThat(instrumentedFourtyTwo.intValue()).isEqualTo(42); + assertThat(metrics.timer(MetricRegistry.name(Number.class, "intValue")).getCount()).isEqualTo(1); + + assertThat(instrumentedFourtyTwo.longValue()).isEqualTo(42); + assertThat(metrics.timer(MetricRegistry.name(Number.class, "longValue")).getCount()).isEqualTo(1); + + verify(mockHandler, times(2)).onSuccess(any(InvocationContext.class), any(Object.class)); + } + + @Test + public void testCannotWrapFinalConcreteType() throws Exception { + try { + Instrumentation.builder(Integer.class, Integer.valueOf(42)) + .withMetrics(metrics) + .withHandler(mockHandler) + .build(); + fail("Integer is a final type and cannot be instrumented"); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()).isEqualTo( + "Cannot subclass final class java.lang.Integer"); + } + + verifyNoMoreInteractions(mockHandler); + } + @Test(expected = NullPointerException.class) public void testNullInterface() { //noinspection ConstantConditions